Skip to content

Commit

Permalink
feat: update pascalCaseConstructId rules
Browse files Browse the repository at this point in the history
  • Loading branch information
ren-yamanashi committed Nov 30, 2024
1 parent 0e83f07 commit 9179479
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 54 deletions.
122 changes: 88 additions & 34 deletions src/__tests__/pascal-case-construct-id.test.ts
Original file line number Diff line number Diff line change
@@ -1,71 +1,125 @@
import { RuleTester } from "eslint";
import { RuleTester } from "@typescript-eslint/rule-tester";

import { pascalCaseConstructId } from "../pascal-case-construct-id.mjs";

const ruleTester = new RuleTester({
languageOptions: { ecmaVersion: "latest", sourceType: "module" },
languageOptions: {
parserOptions: {
projectService: {
allowDefaultProject: ["*.ts*"],
},
},
},
});

ruleTester.run("pascal-case-construct-id", pascalCaseConstructId, {
valid: [
// WHEN: id is empty
{
code: "const test = new TestClass('test');",
},
{
code: "new TestClass('test');",
code: `
class Construct {}
class TestClass extends Construct {
constructor(props: any, id: string) {
super(props, id);
}
}
const test = new TestClass('test');
`,
},
// WHEN: id is object
{
code: "const test = new TestClass('test', {sample: 'sample'});",
},
{
code: "new TestClass('test', {sample: 'sample'});",
code: `
class Construct {}
class TestClass extends Construct {
constructor(props: any, id: string) {
super(props, id);
}
}
const test = new TestClass('test', {sample: 'sample'});`,
},
// WHEN: id is array
{
code: "const test = new TestClass('test', ['sample']);",
},
{
code: "new TestClass('test', ['sample']);",
code: `
class Construct {}
class TestClass extends Construct {
constructor(props: any, id: string) {
super(props, id);
}
}
const test = new TestClass('test', ['sample']);`,
},
// WHEN: id is number
{
code: "const test = new TestClass('test', 1);",
},
{
code: "new TestClass('test', 1);",
code: `
class Construct {}
class TestClass extends Construct {
constructor(props: any, id: string) {
super(props, id);
}
}
const test = new TestClass('test', 1);
`,
},
// WHEN: id is PascalCase
{
code: "const test = new TestClass('test', 'ValidId');",
code: `
class Construct {}
class TestClass extends Construct {
constructor(props: any, id: string) {
super(props, id);
}
}
const test = new TestClass('test', 'ValidId');`,
},
// WHEN: not extends Construct
{
code: "new TestClass('test', 'ValidId');",
code: `
class TestClass {
constructor(public id: string) {}
}
const test = new TestClass('test', 'ValidId');`,
},
],
invalid: [
// WHEN: id is snake_case(double quote)
{
code: 'new TestClass("test", "invalid_id");',
errors: [{ messageId: "pascalCaseConstructId" }],
output: 'new TestClass("test", "InvalidId");',
},
{
code: 'const test = new TestClass("test", "invalid_id");',
code: `
class Construct {}
class TestClass extends Construct {
constructor(props: any, id: string) {
super(props, id);
}
}
const test = new TestClass("test", "invalid_id");`,
errors: [{ messageId: "pascalCaseConstructId" }],
output: 'const test = new TestClass("test", "InvalidId");',
output: `
class Construct {}
class TestClass extends Construct {
constructor(props: any, id: string) {
super(props, id);
}
}
const test = new TestClass("test", "InvalidId");`,
},
// WHEN: id is camelCase(single quote)
{
code: "new TestClass('test', 'invalidId');",
errors: [{ messageId: "pascalCaseConstructId" }],
output: "new TestClass('test', 'InvalidId');",
},
{
code: "const test = new TestClass('test', 'invalidId');",
code: `
class Construct {}
class TestClass extends Construct {
constructor(props: any, id: string) {
super(props, id);
}
}
const test = new TestClass('test', 'invalidId');`,
errors: [{ messageId: "pascalCaseConstructId" }],
output: "const test = new TestClass('test', 'InvalidId');",
output: `
class Construct {}
class TestClass extends Construct {
constructor(props: any, id: string) {
super(props, id);
}
}
const test = new TestClass('test', 'InvalidId');`,
},
],
});
62 changes: 42 additions & 20 deletions src/pascal-case-construct-id.mts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { AST_NODE_TYPES } from "@typescript-eslint/utils";
import { Rule } from "eslint";
import { Expression, Node, SpreadElement } from "estree";
import {
AST_NODE_TYPES,
ESLintUtils,
TSESLint,
TSESTree,
} from "@typescript-eslint/utils";

import { toPascalCase } from "./utils/convertString.mjs";
import { isConstructOrStackType } from "./utils/isConstructOrStackType.mjs";

const QUOTE_TYPE = {
SINGLE: "'",
Expand All @@ -11,13 +15,16 @@ const QUOTE_TYPE = {

type QuoteType = (typeof QUOTE_TYPE)[keyof typeof QUOTE_TYPE];

type Context = TSESLint.RuleContext<"pascalCaseConstructId", []>;

/**
/**
* Enforce PascalCase for Construct ID.
* @param context - The rule context provided by ESLint
* @returns An object containing the AST visitor functions
* @see {@link https://eslint-cdk-plugin.dev/rules/pascal-case-construct-id} - Documentation
*/
export const pascalCaseConstructId: Rule.RuleModule = {
export const pascalCaseConstructId = ESLintUtils.RuleCreator.withoutDocs({
meta: {
type: "problem",
docs: {
Expand All @@ -29,22 +36,37 @@ export const pascalCaseConstructId: Rule.RuleModule = {
schema: [],
fixable: "code",
},
defaultOptions: [],
create(context) {
const parserServices = ESLintUtils.getParserServices(context);
const checker = parserServices.program.getTypeChecker();
return {
ExpressionStatement(node) {
if (node.expression.type !== AST_NODE_TYPES.NewExpression) return;
validateConstructId(node, context, node.expression.arguments);
},
VariableDeclaration(node) {
if (!node.declarations.length) return;
for (const declaration of node.declarations) {
if (declaration.init?.type !== AST_NODE_TYPES.NewExpression) return;
validateConstructId(node, context, declaration.init.arguments);
// ExpressionStatement(node) {
// if (node.expression.type !== AST_NODE_TYPES.NewExpression) return;
// validateConstructId(node, context, node.expression.arguments);
// },
// VariableDeclaration(node) {
// if (!node.declarations.length) return;
// for (const declaration of node.declarations) {
// if (declaration.init?.type !== AST_NODE_TYPES.NewExpression) return;
// validateConstructId(node, context, declaration.init.arguments);
// }
// },
NewExpression(node) {
const type = checker.getTypeAtLocation(
parserServices.esTreeNodeToTSNodeMap.get(node)
);
if (!isConstructOrStackType(type)) {
return;
}

if (node.arguments.length < 2) return;

validateConstructId(node, context, node);
},
};
},
};
});

/**
* check if the string is PascalCase
Expand All @@ -58,15 +80,15 @@ const isPascalCase = (str: string) => {
/**
* Check the construct ID is PascalCase
*/
const validateConstructId = <T extends Node>(
node: T,
context: Rule.RuleContext,
args: (SpreadElement | Expression)[]
const validateConstructId = (
node: TSESTree.Node,
context: Context,
expression: TSESTree.NewExpression
) => {
if (args.length < 2) return;
if (expression.arguments.length < 2) return;

// NOTE: Treat the second argument as ID
const secondArg = args[1];
const secondArg = expression.arguments[1];
if (
secondArg.type !== AST_NODE_TYPES.Literal ||
typeof secondArg.value !== "string"
Expand Down

0 comments on commit 9179479

Please sign in to comment.