eslint-cdk-plugin 1.0.4 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -193779,19 +193779,18 @@ const getConstructorPropertyNames = (type) => {
193779
193779
  return constructor.parameters.map((param) => param.name.getText());
193780
193780
  };
193781
193781
 
193782
- const isConstructOrStackType = (type) => {
193782
+ const isConstructOrStackType = (type, ignoredClasses = ["App", "Stage"]) => {
193783
+ if (ignoredClasses.includes(type.symbol?.name ?? "")) return false;
193783
193784
  return isTargetSuperClassType(
193784
193785
  type,
193785
193786
  ["Construct", "Stack"],
193786
193787
  isConstructOrStackType
193787
193788
  );
193788
193789
  };
193789
- const isConstructType = (type) => {
193790
+ const isConstructType = (type, ignoredClasses = ["App", "Stage", "Stack"]) => {
193791
+ if (ignoredClasses.includes(type.symbol?.name ?? "")) return false;
193790
193792
  return isTargetSuperClassType(type, ["Construct"], isConstructType);
193791
193793
  };
193792
- const isStackType = (type) => {
193793
- return isTargetSuperClassType(type, ["Stack"], isStackType);
193794
- };
193795
193794
  const isTargetSuperClassType = (type, targetSuperClasses, typeCheckFunction) => {
193796
193795
  if (!type.symbol) return false;
193797
193796
  if (targetSuperClasses.some((suffix) => type.symbol.name.endsWith(suffix))) {
@@ -194317,9 +194316,7 @@ const noVariableConstructId = utils.ESLintUtils.RuleCreator.withoutDocs({
194317
194316
  return {
194318
194317
  NewExpression(node) {
194319
194318
  const type = parserServices.getTypeAtLocation(node);
194320
- if (!isConstructType(type) || isStackType(type) || node.arguments.length < 2) {
194321
- return;
194322
- }
194319
+ if (!isConstructType(type) || node.arguments.length < 2) return;
194323
194320
  const constructorPropertyNames = getConstructorPropertyNames(type);
194324
194321
  if (constructorPropertyNames[1] !== "id") return;
194325
194322
  validateConstructId$1(node, context);
@@ -194405,6 +194402,118 @@ const validateConstructId = (node, context) => {
194405
194402
  });
194406
194403
  };
194407
194404
 
194405
+ const propsNameConvention = utils.ESLintUtils.RuleCreator.withoutDocs({
194406
+ meta: {
194407
+ type: "problem",
194408
+ docs: {
194409
+ description: "Enforce props interface name to follow ${ConstructName}Props format"
194410
+ },
194411
+ schema: [],
194412
+ messages: {
194413
+ invalidPropsName: "Props interface name '{{ interfaceName }}' should follow '${ConstructName}Props' format. Expected '{{ expectedName }}'."
194414
+ }
194415
+ },
194416
+ defaultOptions: [],
194417
+ create(context) {
194418
+ const parserServices = utils.ESLintUtils.getParserServices(context);
194419
+ return {
194420
+ ClassDeclaration(node) {
194421
+ if (!node.id || !node.superClass) return;
194422
+ const type = parserServices.getTypeAtLocation(node.superClass);
194423
+ if (!isConstructType(type)) return;
194424
+ const constructor = node.body.body.find(
194425
+ (member) => member.type === utils.AST_NODE_TYPES.MethodDefinition && member.kind === "constructor"
194426
+ );
194427
+ const propsParam = constructor?.value.params?.[2];
194428
+ if (propsParam?.type !== utils.AST_NODE_TYPES.Identifier) return;
194429
+ const typeAnnotation = propsParam.typeAnnotation;
194430
+ if (typeAnnotation?.type !== utils.AST_NODE_TYPES.TSTypeAnnotation) return;
194431
+ const typeNode = typeAnnotation.typeAnnotation;
194432
+ if (typeNode.type !== utils.AST_NODE_TYPES.TSTypeReference) return;
194433
+ const propsTypeName = typeNode.typeName;
194434
+ if (propsTypeName.type !== utils.AST_NODE_TYPES.Identifier) return;
194435
+ const constructName = node.id.name;
194436
+ const expectedPropsName = `${constructName}Props`;
194437
+ if (propsTypeName.name !== expectedPropsName) {
194438
+ context.report({
194439
+ node: propsTypeName,
194440
+ messageId: "invalidPropsName",
194441
+ data: {
194442
+ interfaceName: propsTypeName.name,
194443
+ expectedName: expectedPropsName
194444
+ }
194445
+ });
194446
+ }
194447
+ }
194448
+ };
194449
+ }
194450
+ });
194451
+
194452
+ const requireJSDoc = utils.ESLintUtils.RuleCreator.withoutDocs({
194453
+ meta: {
194454
+ type: "problem",
194455
+ docs: {
194456
+ description: "Require JSDoc comments for interface properties and public properties in Construct classes"
194457
+ },
194458
+ messages: {
194459
+ missingJSDoc: "Property '{{ propertyName }}' should have a JSDoc comment."
194460
+ },
194461
+ schema: []
194462
+ },
194463
+ defaultOptions: [],
194464
+ create(context) {
194465
+ const parserServices = utils.ESLintUtils.getParserServices(context);
194466
+ return {
194467
+ TSPropertySignature(node) {
194468
+ if (node.key.type !== utils.AST_NODE_TYPES.Identifier || node.parent?.type !== utils.AST_NODE_TYPES.TSInterfaceBody) {
194469
+ return;
194470
+ }
194471
+ const sourceCode = context.sourceCode;
194472
+ const comments = sourceCode.getCommentsBefore(node);
194473
+ const hasJSDoc = comments.some(
194474
+ ({ type, value }) => type === utils.AST_TOKEN_TYPES.Block && value.startsWith("*")
194475
+ );
194476
+ if (!hasJSDoc) {
194477
+ context.report({
194478
+ node,
194479
+ messageId: "missingJSDoc",
194480
+ data: {
194481
+ propertyName: node.key.name
194482
+ }
194483
+ });
194484
+ }
194485
+ },
194486
+ PropertyDefinition(node) {
194487
+ if (node.key.type !== utils.AST_NODE_TYPES.Identifier || node.parent.type !== utils.AST_NODE_TYPES.ClassBody) {
194488
+ return;
194489
+ }
194490
+ const classDeclaration = node.parent.parent;
194491
+ if (classDeclaration.type !== utils.AST_NODE_TYPES.ClassDeclaration || !classDeclaration.superClass) {
194492
+ return;
194493
+ }
194494
+ const classType = parserServices.getTypeAtLocation(classDeclaration);
194495
+ if (!isConstructType(classType) || node.accessibility !== "public") {
194496
+ return;
194497
+ }
194498
+ const sourceCode = context.sourceCode;
194499
+ const comments = sourceCode.getCommentsBefore(node);
194500
+ const hasJSDoc = comments.some(
194501
+ ({ type, value }) => type === utils.AST_TOKEN_TYPES.Block && value.startsWith("*")
194502
+ );
194503
+ if (!hasJSDoc) {
194504
+ context.report({
194505
+ node,
194506
+ messageId: "missingJSDoc",
194507
+ data: {
194508
+ propertyName: node.key.name
194509
+ }
194510
+ });
194511
+ }
194512
+ }
194513
+ };
194514
+ }
194515
+ });
194516
+
194408
194517
  const requirePassingThis = utils.ESLintUtils.RuleCreator.withoutDocs({
194409
194518
  meta: {
194410
194519
  type: "problem",
@@ -194423,9 +194532,7 @@ const requirePassingThis = utils.ESLintUtils.RuleCreator.withoutDocs({
194423
194532
  return {
194424
194533
  NewExpression(node) {
194425
194534
  const type = parserServices.getTypeAtLocation(node);
194426
- if (!isConstructType(type) || isStackType(type) || !node.arguments.length) {
194427
- return;
194428
- }
194535
+ if (!isConstructType(type) || !node.arguments.length) return;
194429
194536
  const argument = node.arguments[0];
194430
194537
  if (argument.type === utils.AST_NODE_TYPES.ThisExpression) return;
194431
194538
  const constructorPropertyNames = getConstructorPropertyNames(type);
@@ -194442,6 +194549,48 @@ const requirePassingThis = utils.ESLintUtils.RuleCreator.withoutDocs({
194442
194549
  }
194443
194550
  });
194444
194551
 
194552
+ const requirePropsDefaultDoc = utils.ESLintUtils.RuleCreator.withoutDocs({
194553
+ meta: {
194554
+ type: "problem",
194555
+ docs: {
194556
+ description: "Require @default JSDoc for optional properties in interfaces ending with 'Props'"
194557
+ },
194558
+ schema: [],
194559
+ messages: {
194560
+ missingDefaultDoc: "Optional property '{{ propertyName }}' in Props interface must have @default JSDoc documentation"
194561
+ }
194562
+ },
194563
+ defaultOptions: [],
194564
+ create(context) {
194565
+ return {
194566
+ TSPropertySignature(node) {
194567
+ if (!node.optional) return;
194568
+ const parent = node.parent?.parent;
194569
+ if (parent?.type !== utils.AST_NODE_TYPES.TSInterfaceDeclaration) {
194570
+ return;
194571
+ }
194572
+ if (parent.id.type !== utils.AST_NODE_TYPES.Identifier || !parent.id.name.endsWith("Props")) {
194573
+ return;
194574
+ }
194575
+ const sourceCode = context.sourceCode;
194576
+ const comments = sourceCode.getCommentsBefore(node);
194577
+ const hasDefaultDoc = comments.some(
194578
+ (comment) => comment.type === utils.AST_TOKEN_TYPES.Block && comment.value.includes("*") && comment.value.includes("@default")
194579
+ );
194580
+ if (!hasDefaultDoc) {
194581
+ context.report({
194582
+ node,
194583
+ messageId: "missingDefaultDoc",
194584
+ data: {
194585
+ propertyName: node.key.type === utils.AST_NODE_TYPES.Identifier ? node.key.name : "unknown"
194586
+ }
194587
+ });
194588
+ }
194589
+ }
194590
+ };
194591
+ }
194592
+ });
194593
+
194445
194594
  const rules = {
194446
194595
  "no-class-in-interface": noClassInInterface,
194447
194596
  "no-construct-stack-suffix": noConstructStackSuffix,
@@ -194452,6 +194601,9 @@ const rules = {
194452
194601
  "no-mutable-props-interface": noMutablePropsInterface,
194453
194602
  "require-passing-this": requirePassingThis,
194454
194603
  "no-variable-construct-id": noVariableConstructId,
194604
+ "require-jsdoc": requireJSDoc,
194605
+ "require-props-default-doc": requirePropsDefaultDoc,
194606
+ "props-name-convention": propsNameConvention,
194455
194607
  "no-import-private": noImportPrivate
194456
194608
  };
194457
194609
  const configs = {
@@ -194466,7 +194618,8 @@ const configs = {
194466
194618
  "cdk/require-passing-this": "error",
194467
194619
  "cdk/no-variable-construct-id": "error",
194468
194620
  "cdk/no-mutable-public-fields": "warn",
194469
- "cdk/no-mutable-props-interface": "warn"
194621
+ "cdk/no-mutable-props-interface": "warn",
194622
+ "cdk/props-name-convention": "warn"
194470
194623
  }
194471
194624
  },
194472
194625
  strict: {
@@ -194481,7 +194634,10 @@ const configs = {
194481
194634
  "cdk/no-variable-construct-id": "error",
194482
194635
  "cdk/no-mutable-public-fields": "error",
194483
194636
  "cdk/no-mutable-props-interface": "error",
194484
- "cdk/no-import-private": "error"
194637
+ "cdk/no-import-private": "error",
194638
+ "cdk/require-props-default-doc": "error",
194639
+ "cdk/props-name-convention": "error",
194640
+ "cdk/require-jsdoc": "error"
194485
194641
  }
194486
194642
  }
194487
194643
  };
package/dist/index.d.ts CHANGED
@@ -8,6 +8,9 @@ declare const rules: {
8
8
  "no-mutable-props-interface": import("@typescript-eslint/utils/ts-eslint").RuleModule<"noMutablePropsInterface", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
9
9
  "require-passing-this": import("@typescript-eslint/utils/ts-eslint").RuleModule<"requirePassingThis", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
10
10
  "no-variable-construct-id": import("@typescript-eslint/utils/ts-eslint").RuleModule<"noVariableConstructId", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
11
+ "require-jsdoc": import("@typescript-eslint/utils/ts-eslint").RuleModule<"missingJSDoc", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
12
+ "require-props-default-doc": import("@typescript-eslint/utils/ts-eslint").RuleModule<"missingDefaultDoc", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
13
+ "props-name-convention": import("@typescript-eslint/utils/ts-eslint").RuleModule<"invalidPropsName", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
11
14
  "no-import-private": import("eslint").Rule.RuleModule;
12
15
  };
13
16
  declare const configs: {
@@ -23,6 +26,7 @@ declare const configs: {
23
26
  "cdk/no-variable-construct-id": string;
24
27
  "cdk/no-mutable-public-fields": string;
25
28
  "cdk/no-mutable-props-interface": string;
29
+ "cdk/props-name-convention": string;
26
30
  };
27
31
  };
28
32
  strict: {
@@ -38,6 +42,9 @@ declare const configs: {
38
42
  "cdk/no-mutable-public-fields": string;
39
43
  "cdk/no-mutable-props-interface": string;
40
44
  "cdk/no-import-private": string;
45
+ "cdk/require-props-default-doc": string;
46
+ "cdk/props-name-convention": string;
47
+ "cdk/require-jsdoc": string;
41
48
  };
42
49
  };
43
50
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAWA,QAAA,MAAM,KAAK;;;;;;;;;;;CAWV,CAAC;AAEF,QAAA,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BZ,CAAC;AAEF,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAE1B,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,OAAO,KAAK,CAAC;IACpB,OAAO,EAAE,OAAO,OAAO,CAAC;CACzB;AAED,QAAA,MAAM,eAAe,EAAE,eAGtB,CAAC;AAEF,eAAe,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAcA,QAAA,MAAM,KAAK;;;;;;;;;;;;;;CAcV,CAAC;AAEF,QAAA,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkCZ,CAAC;AAEF,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAE1B,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,OAAO,KAAK,CAAC;IACpB,OAAO,EAAE,OAAO,OAAO,CAAC;CACzB;AAED,QAAA,MAAM,eAAe,EAAE,eAGtB,CAAC;AAEF,eAAe,eAAe,CAAC"}
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { ESLintUtils, AST_NODE_TYPES } from '@typescript-eslint/utils';
1
+ import { ESLintUtils, AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint/utils';
2
2
  import { createRequire } from 'module';
3
3
  import require$$1 from 'fs';
4
4
  import * as path from 'path';
@@ -193756,19 +193756,18 @@ const getConstructorPropertyNames = (type) => {
193756
193756
  return constructor.parameters.map((param) => param.name.getText());
193757
193757
  };
193758
193758
 
193759
- const isConstructOrStackType = (type) => {
193759
+ const isConstructOrStackType = (type, ignoredClasses = ["App", "Stage"]) => {
193760
+ if (ignoredClasses.includes(type.symbol?.name ?? "")) return false;
193760
193761
  return isTargetSuperClassType(
193761
193762
  type,
193762
193763
  ["Construct", "Stack"],
193763
193764
  isConstructOrStackType
193764
193765
  );
193765
193766
  };
193766
- const isConstructType = (type) => {
193767
+ const isConstructType = (type, ignoredClasses = ["App", "Stage", "Stack"]) => {
193768
+ if (ignoredClasses.includes(type.symbol?.name ?? "")) return false;
193767
193769
  return isTargetSuperClassType(type, ["Construct"], isConstructType);
193768
193770
  };
193769
- const isStackType = (type) => {
193770
- return isTargetSuperClassType(type, ["Stack"], isStackType);
193771
- };
193772
193771
  const isTargetSuperClassType = (type, targetSuperClasses, typeCheckFunction) => {
193773
193772
  if (!type.symbol) return false;
193774
193773
  if (targetSuperClasses.some((suffix) => type.symbol.name.endsWith(suffix))) {
@@ -194294,9 +194293,7 @@ const noVariableConstructId = ESLintUtils.RuleCreator.withoutDocs({
194294
194293
  return {
194295
194294
  NewExpression(node) {
194296
194295
  const type = parserServices.getTypeAtLocation(node);
194297
- if (!isConstructType(type) || isStackType(type) || node.arguments.length < 2) {
194298
- return;
194299
- }
194296
+ if (!isConstructType(type) || node.arguments.length < 2) return;
194300
194297
  const constructorPropertyNames = getConstructorPropertyNames(type);
194301
194298
  if (constructorPropertyNames[1] !== "id") return;
194302
194299
  validateConstructId$1(node, context);
@@ -194382,6 +194379,118 @@ const validateConstructId = (node, context) => {
194382
194379
  });
194383
194380
  };
194384
194381
 
194382
+ const propsNameConvention = ESLintUtils.RuleCreator.withoutDocs({
194383
+ meta: {
194384
+ type: "problem",
194385
+ docs: {
194386
+ description: "Enforce props interface name to follow ${ConstructName}Props format"
194387
+ },
194388
+ schema: [],
194389
+ messages: {
194390
+ invalidPropsName: "Props interface name '{{ interfaceName }}' should follow '${ConstructName}Props' format. Expected '{{ expectedName }}'."
194391
+ }
194392
+ },
194393
+ defaultOptions: [],
194394
+ create(context) {
194395
+ const parserServices = ESLintUtils.getParserServices(context);
194396
+ return {
194397
+ ClassDeclaration(node) {
194398
+ if (!node.id || !node.superClass) return;
194399
+ const type = parserServices.getTypeAtLocation(node.superClass);
194400
+ if (!isConstructType(type)) return;
194401
+ const constructor = node.body.body.find(
194402
+ (member) => member.type === AST_NODE_TYPES.MethodDefinition && member.kind === "constructor"
194403
+ );
194404
+ const propsParam = constructor?.value.params?.[2];
194405
+ if (propsParam?.type !== AST_NODE_TYPES.Identifier) return;
194406
+ const typeAnnotation = propsParam.typeAnnotation;
194407
+ if (typeAnnotation?.type !== AST_NODE_TYPES.TSTypeAnnotation) return;
194408
+ const typeNode = typeAnnotation.typeAnnotation;
194409
+ if (typeNode.type !== AST_NODE_TYPES.TSTypeReference) return;
194410
+ const propsTypeName = typeNode.typeName;
194411
+ if (propsTypeName.type !== AST_NODE_TYPES.Identifier) return;
194412
+ const constructName = node.id.name;
194413
+ const expectedPropsName = `${constructName}Props`;
194414
+ if (propsTypeName.name !== expectedPropsName) {
194415
+ context.report({
194416
+ node: propsTypeName,
194417
+ messageId: "invalidPropsName",
194418
+ data: {
194419
+ interfaceName: propsTypeName.name,
194420
+ expectedName: expectedPropsName
194421
+ }
194422
+ });
194423
+ }
194424
+ }
194425
+ };
194426
+ }
194427
+ });
194428
+
194429
+ const requireJSDoc = ESLintUtils.RuleCreator.withoutDocs({
194430
+ meta: {
194431
+ type: "problem",
194432
+ docs: {
194433
+ description: "Require JSDoc comments for interface properties and public properties in Construct classes"
194434
+ },
194435
+ messages: {
194436
+ missingJSDoc: "Property '{{ propertyName }}' should have a JSDoc comment."
194437
+ },
194438
+ schema: []
194439
+ },
194440
+ defaultOptions: [],
194441
+ create(context) {
194442
+ const parserServices = ESLintUtils.getParserServices(context);
194443
+ return {
194444
+ TSPropertySignature(node) {
194445
+ if (node.key.type !== AST_NODE_TYPES.Identifier || node.parent?.type !== AST_NODE_TYPES.TSInterfaceBody) {
194446
+ return;
194447
+ }
194448
+ const sourceCode = context.sourceCode;
194449
+ const comments = sourceCode.getCommentsBefore(node);
194450
+ const hasJSDoc = comments.some(
194451
+ ({ type, value }) => type === AST_TOKEN_TYPES.Block && value.startsWith("*")
194452
+ );
194453
+ if (!hasJSDoc) {
194454
+ context.report({
194455
+ node,
194456
+ messageId: "missingJSDoc",
194457
+ data: {
194458
+ propertyName: node.key.name
194459
+ }
194460
+ });
194461
+ }
194462
+ },
194463
+ PropertyDefinition(node) {
194464
+ if (node.key.type !== AST_NODE_TYPES.Identifier || node.parent.type !== AST_NODE_TYPES.ClassBody) {
194465
+ return;
194466
+ }
194467
+ const classDeclaration = node.parent.parent;
194468
+ if (classDeclaration.type !== AST_NODE_TYPES.ClassDeclaration || !classDeclaration.superClass) {
194469
+ return;
194470
+ }
194471
+ const classType = parserServices.getTypeAtLocation(classDeclaration);
194472
+ if (!isConstructType(classType) || node.accessibility !== "public") {
194473
+ return;
194474
+ }
194475
+ const sourceCode = context.sourceCode;
194476
+ const comments = sourceCode.getCommentsBefore(node);
194477
+ const hasJSDoc = comments.some(
194478
+ ({ type, value }) => type === AST_TOKEN_TYPES.Block && value.startsWith("*")
194479
+ );
194480
+ if (!hasJSDoc) {
194481
+ context.report({
194482
+ node,
194483
+ messageId: "missingJSDoc",
194484
+ data: {
194485
+ propertyName: node.key.name
194486
+ }
194487
+ });
194488
+ }
194489
+ }
194490
+ };
194491
+ }
194492
+ });
194493
+
194385
194494
  const requirePassingThis = ESLintUtils.RuleCreator.withoutDocs({
194386
194495
  meta: {
194387
194496
  type: "problem",
@@ -194400,9 +194509,7 @@ const requirePassingThis = ESLintUtils.RuleCreator.withoutDocs({
194400
194509
  return {
194401
194510
  NewExpression(node) {
194402
194511
  const type = parserServices.getTypeAtLocation(node);
194403
- if (!isConstructType(type) || isStackType(type) || !node.arguments.length) {
194404
- return;
194405
- }
194512
+ if (!isConstructType(type) || !node.arguments.length) return;
194406
194513
  const argument = node.arguments[0];
194407
194514
  if (argument.type === AST_NODE_TYPES.ThisExpression) return;
194408
194515
  const constructorPropertyNames = getConstructorPropertyNames(type);
@@ -194419,6 +194526,48 @@ const requirePassingThis = ESLintUtils.RuleCreator.withoutDocs({
194419
194526
  }
194420
194527
  });
194421
194528
 
194529
+ const requirePropsDefaultDoc = ESLintUtils.RuleCreator.withoutDocs({
194530
+ meta: {
194531
+ type: "problem",
194532
+ docs: {
194533
+ description: "Require @default JSDoc for optional properties in interfaces ending with 'Props'"
194534
+ },
194535
+ schema: [],
194536
+ messages: {
194537
+ missingDefaultDoc: "Optional property '{{ propertyName }}' in Props interface must have @default JSDoc documentation"
194538
+ }
194539
+ },
194540
+ defaultOptions: [],
194541
+ create(context) {
194542
+ return {
194543
+ TSPropertySignature(node) {
194544
+ if (!node.optional) return;
194545
+ const parent = node.parent?.parent;
194546
+ if (parent?.type !== AST_NODE_TYPES.TSInterfaceDeclaration) {
194547
+ return;
194548
+ }
194549
+ if (parent.id.type !== AST_NODE_TYPES.Identifier || !parent.id.name.endsWith("Props")) {
194550
+ return;
194551
+ }
194552
+ const sourceCode = context.sourceCode;
194553
+ const comments = sourceCode.getCommentsBefore(node);
194554
+ const hasDefaultDoc = comments.some(
194555
+ (comment) => comment.type === AST_TOKEN_TYPES.Block && comment.value.includes("*") && comment.value.includes("@default")
194556
+ );
194557
+ if (!hasDefaultDoc) {
194558
+ context.report({
194559
+ node,
194560
+ messageId: "missingDefaultDoc",
194561
+ data: {
194562
+ propertyName: node.key.type === AST_NODE_TYPES.Identifier ? node.key.name : "unknown"
194563
+ }
194564
+ });
194565
+ }
194566
+ }
194567
+ };
194568
+ }
194569
+ });
194570
+
194422
194571
  const rules = {
194423
194572
  "no-class-in-interface": noClassInInterface,
194424
194573
  "no-construct-stack-suffix": noConstructStackSuffix,
@@ -194429,6 +194578,9 @@ const rules = {
194429
194578
  "no-mutable-props-interface": noMutablePropsInterface,
194430
194579
  "require-passing-this": requirePassingThis,
194431
194580
  "no-variable-construct-id": noVariableConstructId,
194581
+ "require-jsdoc": requireJSDoc,
194582
+ "require-props-default-doc": requirePropsDefaultDoc,
194583
+ "props-name-convention": propsNameConvention,
194432
194584
  "no-import-private": noImportPrivate
194433
194585
  };
194434
194586
  const configs = {
@@ -194443,7 +194595,8 @@ const configs = {
194443
194595
  "cdk/require-passing-this": "error",
194444
194596
  "cdk/no-variable-construct-id": "error",
194445
194597
  "cdk/no-mutable-public-fields": "warn",
194446
- "cdk/no-mutable-props-interface": "warn"
194598
+ "cdk/no-mutable-props-interface": "warn",
194599
+ "cdk/props-name-convention": "warn"
194447
194600
  }
194448
194601
  },
194449
194602
  strict: {
@@ -194458,7 +194611,10 @@ const configs = {
194458
194611
  "cdk/no-variable-construct-id": "error",
194459
194612
  "cdk/no-mutable-public-fields": "error",
194460
194613
  "cdk/no-mutable-props-interface": "error",
194461
- "cdk/no-import-private": "error"
194614
+ "cdk/no-import-private": "error",
194615
+ "cdk/require-props-default-doc": "error",
194616
+ "cdk/props-name-convention": "error",
194617
+ "cdk/require-jsdoc": "error"
194462
194618
  }
194463
194619
  }
194464
194620
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-cdk-plugin",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
4
4
  "description": "eslint plugin for AWS CDK projects",
5
5
  "main": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.ts",
package/src/index.ts CHANGED
@@ -7,7 +7,10 @@ import { noParentNameConstructIdMatch } from "./rules/no-parent-name-construct-i
7
7
  import { noPublicClassFields } from "./rules/no-public-class-fields";
8
8
  import { noVariableConstructId } from "./rules/no-variable-construct-id";
9
9
  import { pascalCaseConstructId } from "./rules/pascal-case-construct-id";
10
+ import { propsNameConvention } from "./rules/props-name-convention";
11
+ import { requireJSDoc } from "./rules/require-jsdoc";
10
12
  import { requirePassingThis } from "./rules/require-passing-this";
13
+ import { requirePropsDefaultDoc } from "./rules/require-props-default-doc";
11
14
 
12
15
  const rules = {
13
16
  "no-class-in-interface": noClassInInterface,
@@ -19,6 +22,9 @@ const rules = {
19
22
  "no-mutable-props-interface": noMutablePropsInterface,
20
23
  "require-passing-this": requirePassingThis,
21
24
  "no-variable-construct-id": noVariableConstructId,
25
+ "require-jsdoc": requireJSDoc,
26
+ "require-props-default-doc": requirePropsDefaultDoc,
27
+ "props-name-convention": propsNameConvention,
22
28
  "no-import-private": noImportPrivate,
23
29
  };
24
30
 
@@ -35,6 +41,7 @@ const configs = {
35
41
  "cdk/no-variable-construct-id": "error",
36
42
  "cdk/no-mutable-public-fields": "warn",
37
43
  "cdk/no-mutable-props-interface": "warn",
44
+ "cdk/props-name-convention": "warn",
38
45
  },
39
46
  },
40
47
  strict: {
@@ -50,6 +57,9 @@ const configs = {
50
57
  "cdk/no-mutable-public-fields": "error",
51
58
  "cdk/no-mutable-props-interface": "error",
52
59
  "cdk/no-import-private": "error",
60
+ "cdk/require-props-default-doc": "error",
61
+ "cdk/props-name-convention": "error",
62
+ "cdk/require-jsdoc": "error",
53
63
  },
54
64
  },
55
65
  };
@@ -6,7 +6,7 @@ import {
6
6
  } from "@typescript-eslint/utils";
7
7
 
8
8
  import { getConstructorPropertyNames } from "../utils/parseType";
9
- import { isConstructType, isStackType } from "../utils/typeCheck";
9
+ import { isConstructType } from "../utils/typeCheck";
10
10
 
11
11
  type Context = TSESLint.RuleContext<"noVariableConstructId", []>;
12
12
 
@@ -33,13 +33,7 @@ export const noVariableConstructId = ESLintUtils.RuleCreator.withoutDocs({
33
33
  NewExpression(node) {
34
34
  const type = parserServices.getTypeAtLocation(node);
35
35
 
36
- if (
37
- !isConstructType(type) ||
38
- isStackType(type) ||
39
- node.arguments.length < 2
40
- ) {
41
- return;
42
- }
36
+ if (!isConstructType(type) || node.arguments.length < 2) return;
43
37
 
44
38
  const constructorPropertyNames = getConstructorPropertyNames(type);
45
39
  if (constructorPropertyNames[1] !== "id") return;
@@ -0,0 +1,75 @@
1
+ import {
2
+ AST_NODE_TYPES,
3
+ ESLintUtils,
4
+ TSESTree,
5
+ } from "@typescript-eslint/utils";
6
+
7
+ import { isConstructType } from "../utils/typeCheck";
8
+
9
+ /**
10
+ * Enforces a naming convention for props interfaces in Construct classes
11
+ * @param context - The rule context provided by ESLint
12
+ * @returns An object containing the AST visitor functions
13
+ * @see {@link https://eslint-cdk-plugin.dev/rules/props-name-convention} - Documentation
14
+ */
15
+ export const propsNameConvention = ESLintUtils.RuleCreator.withoutDocs({
16
+ meta: {
17
+ type: "problem",
18
+ docs: {
19
+ description:
20
+ "Enforce props interface name to follow ${ConstructName}Props format",
21
+ },
22
+ schema: [],
23
+ messages: {
24
+ invalidPropsName:
25
+ "Props interface name '{{ interfaceName }}' should follow '${ConstructName}Props' format. Expected '{{ expectedName }}'.",
26
+ },
27
+ },
28
+ defaultOptions: [],
29
+ create(context) {
30
+ const parserServices = ESLintUtils.getParserServices(context);
31
+ return {
32
+ ClassDeclaration(node) {
33
+ if (!node.id || !node.superClass) return;
34
+
35
+ const type = parserServices.getTypeAtLocation(node.superClass);
36
+ if (!isConstructType(type)) return;
37
+
38
+ // NOTE: check constructor parameter
39
+ const constructor = node.body.body.find(
40
+ (member): member is TSESTree.MethodDefinition =>
41
+ member.type === AST_NODE_TYPES.MethodDefinition &&
42
+ member.kind === "constructor"
43
+ );
44
+
45
+ const propsParam = constructor?.value.params?.[2];
46
+ if (propsParam?.type !== AST_NODE_TYPES.Identifier) return;
47
+
48
+ const typeAnnotation = propsParam.typeAnnotation;
49
+ if (typeAnnotation?.type !== AST_NODE_TYPES.TSTypeAnnotation) return;
50
+
51
+ const typeNode = typeAnnotation.typeAnnotation;
52
+ if (typeNode.type !== AST_NODE_TYPES.TSTypeReference) return;
53
+
54
+ const propsTypeName = typeNode.typeName;
55
+ if (propsTypeName.type !== AST_NODE_TYPES.Identifier) return;
56
+
57
+ // NOTE: create valid props name
58
+ const constructName = node.id.name;
59
+ const expectedPropsName = `${constructName}Props`;
60
+
61
+ // NOTE: error when props name is not expected format
62
+ if (propsTypeName.name !== expectedPropsName) {
63
+ context.report({
64
+ node: propsTypeName,
65
+ messageId: "invalidPropsName",
66
+ data: {
67
+ interfaceName: propsTypeName.name,
68
+ expectedName: expectedPropsName,
69
+ },
70
+ });
71
+ }
72
+ },
73
+ };
74
+ },
75
+ });
@@ -0,0 +1,99 @@
1
+ import {
2
+ AST_NODE_TYPES,
3
+ AST_TOKEN_TYPES,
4
+ ESLintUtils,
5
+ } from "@typescript-eslint/utils";
6
+
7
+ import { isConstructType } from "../utils/typeCheck";
8
+
9
+ /**
10
+ * Require JSDoc comments for interface properties and public properties in Construct classes
11
+ * @param context - The rule context provided by ESLint
12
+ * @returns An object containing the AST visitor functions
13
+ * @see {@link https://eslint-cdk-plugin.dev/rules/require-jsdoc} - Documentation
14
+ */
15
+ export const requireJSDoc = ESLintUtils.RuleCreator.withoutDocs({
16
+ meta: {
17
+ type: "problem",
18
+ docs: {
19
+ description:
20
+ "Require JSDoc comments for interface properties and public properties in Construct classes",
21
+ },
22
+ messages: {
23
+ missingJSDoc:
24
+ "Property '{{ propertyName }}' should have a JSDoc comment.",
25
+ },
26
+ schema: [],
27
+ },
28
+ defaultOptions: [],
29
+ create(context) {
30
+ const parserServices = ESLintUtils.getParserServices(context);
31
+ return {
32
+ TSPropertySignature(node) {
33
+ if (
34
+ node.key.type !== AST_NODE_TYPES.Identifier ||
35
+ node.parent?.type !== AST_NODE_TYPES.TSInterfaceBody
36
+ ) {
37
+ return;
38
+ }
39
+
40
+ const sourceCode = context.sourceCode;
41
+ const comments = sourceCode.getCommentsBefore(node);
42
+ const hasJSDoc = comments.some(
43
+ ({ type, value }) =>
44
+ type === AST_TOKEN_TYPES.Block && value.startsWith("*")
45
+ );
46
+
47
+ if (!hasJSDoc) {
48
+ context.report({
49
+ node,
50
+ messageId: "missingJSDoc",
51
+ data: {
52
+ propertyName: node.key.name,
53
+ },
54
+ });
55
+ }
56
+ },
57
+ PropertyDefinition(node) {
58
+ if (
59
+ node.key.type !== AST_NODE_TYPES.Identifier ||
60
+ node.parent.type !== AST_NODE_TYPES.ClassBody
61
+ ) {
62
+ return;
63
+ }
64
+
65
+ // NOTE: Check if the class extends Construct
66
+ const classDeclaration = node.parent.parent;
67
+ if (
68
+ classDeclaration.type !== AST_NODE_TYPES.ClassDeclaration ||
69
+ !classDeclaration.superClass
70
+ ) {
71
+ return;
72
+ }
73
+
74
+ // NOTE: Check if the class extends Construct and the property is public
75
+ const classType = parserServices.getTypeAtLocation(classDeclaration);
76
+ if (!isConstructType(classType) || node.accessibility !== "public") {
77
+ return;
78
+ }
79
+
80
+ const sourceCode = context.sourceCode;
81
+ const comments = sourceCode.getCommentsBefore(node);
82
+ const hasJSDoc = comments.some(
83
+ ({ type, value }) =>
84
+ type === AST_TOKEN_TYPES.Block && value.startsWith("*")
85
+ );
86
+
87
+ if (!hasJSDoc) {
88
+ context.report({
89
+ node,
90
+ messageId: "missingJSDoc",
91
+ data: {
92
+ propertyName: node.key.name,
93
+ },
94
+ });
95
+ }
96
+ },
97
+ };
98
+ },
99
+ });
@@ -1,7 +1,7 @@
1
1
  import { AST_NODE_TYPES, ESLintUtils } from "@typescript-eslint/utils";
2
2
 
3
3
  import { getConstructorPropertyNames } from "../utils/parseType";
4
- import { isConstructType, isStackType } from "../utils/typeCheck";
4
+ import { isConstructType } from "../utils/typeCheck";
5
5
 
6
6
  /**
7
7
  * Enforces that `this` is passed to the constructor
@@ -28,13 +28,7 @@ export const requirePassingThis = ESLintUtils.RuleCreator.withoutDocs({
28
28
  NewExpression(node) {
29
29
  const type = parserServices.getTypeAtLocation(node);
30
30
 
31
- if (
32
- !isConstructType(type) ||
33
- isStackType(type) ||
34
- !node.arguments.length
35
- ) {
36
- return;
37
- }
31
+ if (!isConstructType(type) || !node.arguments.length) return;
38
32
 
39
33
  const argument = node.arguments[0];
40
34
  if (argument.type === AST_NODE_TYPES.ThisExpression) return;
@@ -0,0 +1,72 @@
1
+ import {
2
+ AST_NODE_TYPES,
3
+ AST_TOKEN_TYPES,
4
+ ESLintUtils,
5
+ } from "@typescript-eslint/utils";
6
+
7
+ /**
8
+ * Requires @default JSDoc documentation for optional properties in interfaces ending with 'Props'
9
+ * @param context - The rule context provided by ESLint
10
+ * @returns An object containing the AST visitor functions
11
+ * @see {@link https://eslint-cdk-plugin.dev/rules/require-props-default-doc} - Documentation
12
+ */
13
+ export const requirePropsDefaultDoc = ESLintUtils.RuleCreator.withoutDocs({
14
+ meta: {
15
+ type: "problem",
16
+ docs: {
17
+ description:
18
+ "Require @default JSDoc for optional properties in interfaces ending with 'Props'",
19
+ },
20
+ schema: [],
21
+ messages: {
22
+ missingDefaultDoc:
23
+ "Optional property '{{ propertyName }}' in Props interface must have @default JSDoc documentation",
24
+ },
25
+ },
26
+ defaultOptions: [],
27
+ create(context) {
28
+ return {
29
+ TSPropertySignature(node) {
30
+ // NOTE: Check if the property is optional
31
+ if (!node.optional) return;
32
+
33
+ // NOTE: Check if the parent is an interface
34
+ const parent = node.parent?.parent;
35
+ if (parent?.type !== AST_NODE_TYPES.TSInterfaceDeclaration) {
36
+ return;
37
+ }
38
+
39
+ // NOTE: Check if the interface name ends with 'Props'
40
+ if (
41
+ parent.id.type !== AST_NODE_TYPES.Identifier ||
42
+ !parent.id.name.endsWith("Props")
43
+ ) {
44
+ return;
45
+ }
46
+
47
+ // NOTE: Get JSDoc comments
48
+ const sourceCode = context.sourceCode;
49
+ const comments = sourceCode.getCommentsBefore(node);
50
+ const hasDefaultDoc = comments.some(
51
+ (comment) =>
52
+ comment.type === AST_TOKEN_TYPES.Block &&
53
+ comment.value.includes("*") &&
54
+ comment.value.includes("@default")
55
+ );
56
+
57
+ if (!hasDefaultDoc) {
58
+ context.report({
59
+ node,
60
+ messageId: "missingDefaultDoc",
61
+ data: {
62
+ propertyName:
63
+ node.key.type === AST_NODE_TYPES.Identifier
64
+ ? node.key.name
65
+ : "unknown",
66
+ },
67
+ });
68
+ }
69
+ },
70
+ };
71
+ },
72
+ });
@@ -5,9 +5,14 @@ type SuperClassType = "Construct" | "Stack";
5
5
  /**
6
6
  * Check if the type extends Construct or Stack
7
7
  * @param type - The type to check
8
+ * @param ignoredClasses - Classes that inherit from Construct Class or Stack Class but do not want to be treated as Construct Class or Stack Class
8
9
  * @returns True if the type extends Construct or Stack, otherwise false
9
10
  */
10
- export const isConstructOrStackType = (type: Type): boolean => {
11
+ export const isConstructOrStackType = (
12
+ type: Type,
13
+ ignoredClasses: readonly string[] = ["App", "Stage"] as const
14
+ ): boolean => {
15
+ if (ignoredClasses.includes(type.symbol?.name ?? "")) return false;
11
16
  return isTargetSuperClassType(
12
17
  type,
13
18
  ["Construct", "Stack"],
@@ -18,21 +23,17 @@ export const isConstructOrStackType = (type: Type): boolean => {
18
23
  /**
19
24
  * Check if the type extends Construct
20
25
  * @param type - The type to check
26
+ * @param ignoredClasses - Classes that inherit from Construct Class but do not want to be treated as Construct Class
21
27
  * @returns True if the type extends Construct, otherwise false
22
28
  */
23
- export const isConstructType = (type: Type): boolean => {
29
+ export const isConstructType = (
30
+ type: Type,
31
+ ignoredClasses: readonly string[] = ["App", "Stage", "Stack"] as const
32
+ ): boolean => {
33
+ if (ignoredClasses.includes(type.symbol?.name ?? "")) return false;
24
34
  return isTargetSuperClassType(type, ["Construct"], isConstructType);
25
35
  };
26
36
 
27
- /**
28
- * Check if the type extends Stack
29
- * @param type - The type to check
30
- * @returns True if the type extends Stack, otherwise false
31
- */
32
- export const isStackType = (type: Type): boolean => {
33
- return isTargetSuperClassType(type, ["Stack"], isStackType);
34
- };
35
-
36
37
  /**
37
38
  * Check if the type extends target super class
38
39
  * @param type - The type to check