diff --git a/src/__tests__/pascal-case-construct-id.test.ts b/src/__tests__/pascal-case-construct-id.test.ts index bd8bc72..321c15c 100644 --- a/src/__tests__/pascal-case-construct-id.test.ts +++ b/src/__tests__/pascal-case-construct-id.test.ts @@ -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');`, }, ], }); diff --git a/src/pascal-case-construct-id.mts b/src/pascal-case-construct-id.mts index 3db8b1d..d386f34 100644 --- a/src/pascal-case-construct-id.mts +++ b/src/pascal-case-construct-id.mts @@ -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: "'", @@ -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: { @@ -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 @@ -58,15 +80,15 @@ const isPascalCase = (str: string) => { /** * Check the construct ID is PascalCase */ -const validateConstructId = ( - 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"