@composurecdk/eslint-plugin 0.10.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.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +138 -0
  3. package/dist/commonjs/configs/presets.d.ts +42 -0
  4. package/dist/commonjs/configs/presets.d.ts.map +1 -0
  5. package/dist/commonjs/configs/presets.js +58 -0
  6. package/dist/commonjs/configs/presets.js.map +1 -0
  7. package/dist/commonjs/index.d.ts +84 -0
  8. package/dist/commonjs/index.d.ts.map +1 -0
  9. package/dist/commonjs/index.js +63 -0
  10. package/dist/commonjs/index.js.map +1 -0
  11. package/dist/commonjs/package.json +3 -0
  12. package/dist/commonjs/rules/builder-must-be-tagged.d.ts +15 -0
  13. package/dist/commonjs/rules/builder-must-be-tagged.d.ts.map +1 -0
  14. package/dist/commonjs/rules/builder-must-be-tagged.js +70 -0
  15. package/dist/commonjs/rules/builder-must-be-tagged.js.map +1 -0
  16. package/dist/commonjs/rules/builder-must-implement-copy-state.d.ts +13 -0
  17. package/dist/commonjs/rules/builder-must-implement-copy-state.d.ts.map +1 -0
  18. package/dist/commonjs/rules/builder-must-implement-copy-state.js +123 -0
  19. package/dist/commonjs/rules/builder-must-implement-copy-state.js.map +1 -0
  20. package/dist/commonjs/rules/constraint-metadata-required.d.ts +16 -0
  21. package/dist/commonjs/rules/constraint-metadata-required.d.ts.map +1 -0
  22. package/dist/commonjs/rules/constraint-metadata-required.js +79 -0
  23. package/dist/commonjs/rules/constraint-metadata-required.js.map +1 -0
  24. package/dist/commonjs/rules/index.d.ts +14 -0
  25. package/dist/commonjs/rules/index.d.ts.map +1 -0
  26. package/dist/commonjs/rules/index.js +52 -0
  27. package/dist/commonjs/rules/index.js.map +1 -0
  28. package/dist/commonjs/rules/lib/imports.d.ts +24 -0
  29. package/dist/commonjs/rules/lib/imports.d.ts.map +1 -0
  30. package/dist/commonjs/rules/lib/imports.js +76 -0
  31. package/dist/commonjs/rules/lib/imports.js.map +1 -0
  32. package/dist/commonjs/rules/lifecycle-build-context-required.d.ts +14 -0
  33. package/dist/commonjs/rules/lifecycle-build-context-required.d.ts.map +1 -0
  34. package/dist/commonjs/rules/lifecycle-build-context-required.js +46 -0
  35. package/dist/commonjs/rules/lifecycle-build-context-required.js.map +1 -0
  36. package/dist/commonjs/rules/lifecycle-build-must-forward-context.d.ts +18 -0
  37. package/dist/commonjs/rules/lifecycle-build-must-forward-context.d.ts.map +1 -0
  38. package/dist/commonjs/rules/lifecycle-build-must-forward-context.js +54 -0
  39. package/dist/commonjs/rules/lifecycle-build-must-forward-context.js.map +1 -0
  40. package/dist/commonjs/rules/no-cdk-api-above-floor.d.ts +15 -0
  41. package/dist/commonjs/rules/no-cdk-api-above-floor.d.ts.map +1 -0
  42. package/dist/commonjs/rules/no-cdk-api-above-floor.js +80 -0
  43. package/dist/commonjs/rules/no-cdk-api-above-floor.js.map +1 -0
  44. package/dist/commonjs/rules/no-cjs-incompatible-syntax.d.ts +14 -0
  45. package/dist/commonjs/rules/no-cjs-incompatible-syntax.d.ts.map +1 -0
  46. package/dist/commonjs/rules/no-cjs-incompatible-syntax.js +55 -0
  47. package/dist/commonjs/rules/no-cjs-incompatible-syntax.js.map +1 -0
  48. package/dist/commonjs/rules/no-realm-bound-instanceof.d.ts +25 -0
  49. package/dist/commonjs/rules/no-realm-bound-instanceof.d.ts.map +1 -0
  50. package/dist/commonjs/rules/no-realm-bound-instanceof.js +98 -0
  51. package/dist/commonjs/rules/no-realm-bound-instanceof.js.map +1 -0
  52. package/dist/commonjs/rules/no-typescript-private-modifier.d.ts +17 -0
  53. package/dist/commonjs/rules/no-typescript-private-modifier.d.ts.map +1 -0
  54. package/dist/commonjs/rules/no-typescript-private-modifier.js +91 -0
  55. package/dist/commonjs/rules/no-typescript-private-modifier.js.map +1 -0
  56. package/dist/commonjs/rules/redeclared-prop-must-track-cdk-type.d.ts +17 -0
  57. package/dist/commonjs/rules/redeclared-prop-must-track-cdk-type.d.ts.map +1 -0
  58. package/dist/commonjs/rules/redeclared-prop-must-track-cdk-type.js +152 -0
  59. package/dist/commonjs/rules/redeclared-prop-must-track-cdk-type.js.map +1 -0
  60. package/dist/esm/configs/presets.d.ts +42 -0
  61. package/dist/esm/configs/presets.d.ts.map +1 -0
  62. package/dist/esm/configs/presets.js +55 -0
  63. package/dist/esm/configs/presets.js.map +1 -0
  64. package/dist/esm/index.d.ts +84 -0
  65. package/dist/esm/index.d.ts.map +1 -0
  66. package/dist/esm/index.js +60 -0
  67. package/dist/esm/index.js.map +1 -0
  68. package/dist/esm/package.json +3 -0
  69. package/dist/esm/rules/builder-must-be-tagged.d.ts +15 -0
  70. package/dist/esm/rules/builder-must-be-tagged.d.ts.map +1 -0
  71. package/dist/esm/rules/builder-must-be-tagged.js +67 -0
  72. package/dist/esm/rules/builder-must-be-tagged.js.map +1 -0
  73. package/dist/esm/rules/builder-must-implement-copy-state.d.ts +13 -0
  74. package/dist/esm/rules/builder-must-implement-copy-state.d.ts.map +1 -0
  75. package/dist/esm/rules/builder-must-implement-copy-state.js +120 -0
  76. package/dist/esm/rules/builder-must-implement-copy-state.js.map +1 -0
  77. package/dist/esm/rules/constraint-metadata-required.d.ts +16 -0
  78. package/dist/esm/rules/constraint-metadata-required.d.ts.map +1 -0
  79. package/dist/esm/rules/constraint-metadata-required.js +76 -0
  80. package/dist/esm/rules/constraint-metadata-required.js.map +1 -0
  81. package/dist/esm/rules/index.d.ts +14 -0
  82. package/dist/esm/rules/index.d.ts.map +1 -0
  83. package/dist/esm/rules/index.js +49 -0
  84. package/dist/esm/rules/index.js.map +1 -0
  85. package/dist/esm/rules/lib/imports.d.ts +24 -0
  86. package/dist/esm/rules/lib/imports.d.ts.map +1 -0
  87. package/dist/esm/rules/lib/imports.js +70 -0
  88. package/dist/esm/rules/lib/imports.js.map +1 -0
  89. package/dist/esm/rules/lifecycle-build-context-required.d.ts +14 -0
  90. package/dist/esm/rules/lifecycle-build-context-required.d.ts.map +1 -0
  91. package/dist/esm/rules/lifecycle-build-context-required.js +43 -0
  92. package/dist/esm/rules/lifecycle-build-context-required.js.map +1 -0
  93. package/dist/esm/rules/lifecycle-build-must-forward-context.d.ts +18 -0
  94. package/dist/esm/rules/lifecycle-build-must-forward-context.d.ts.map +1 -0
  95. package/dist/esm/rules/lifecycle-build-must-forward-context.js +51 -0
  96. package/dist/esm/rules/lifecycle-build-must-forward-context.js.map +1 -0
  97. package/dist/esm/rules/no-cdk-api-above-floor.d.ts +15 -0
  98. package/dist/esm/rules/no-cdk-api-above-floor.d.ts.map +1 -0
  99. package/dist/esm/rules/no-cdk-api-above-floor.js +77 -0
  100. package/dist/esm/rules/no-cdk-api-above-floor.js.map +1 -0
  101. package/dist/esm/rules/no-cjs-incompatible-syntax.d.ts +14 -0
  102. package/dist/esm/rules/no-cjs-incompatible-syntax.d.ts.map +1 -0
  103. package/dist/esm/rules/no-cjs-incompatible-syntax.js +52 -0
  104. package/dist/esm/rules/no-cjs-incompatible-syntax.js.map +1 -0
  105. package/dist/esm/rules/no-realm-bound-instanceof.d.ts +25 -0
  106. package/dist/esm/rules/no-realm-bound-instanceof.d.ts.map +1 -0
  107. package/dist/esm/rules/no-realm-bound-instanceof.js +95 -0
  108. package/dist/esm/rules/no-realm-bound-instanceof.js.map +1 -0
  109. package/dist/esm/rules/no-typescript-private-modifier.d.ts +17 -0
  110. package/dist/esm/rules/no-typescript-private-modifier.d.ts.map +1 -0
  111. package/dist/esm/rules/no-typescript-private-modifier.js +88 -0
  112. package/dist/esm/rules/no-typescript-private-modifier.js.map +1 -0
  113. package/dist/esm/rules/redeclared-prop-must-track-cdk-type.d.ts +17 -0
  114. package/dist/esm/rules/redeclared-prop-must-track-cdk-type.d.ts.map +1 -0
  115. package/dist/esm/rules/redeclared-prop-must-track-cdk-type.js +149 -0
  116. package/dist/esm/rules/redeclared-prop-must-track-cdk-type.js.map +1 -0
  117. package/docs/rules/builder-must-be-tagged.md +46 -0
  118. package/docs/rules/builder-must-implement-copy-state.md +50 -0
  119. package/docs/rules/constraint-metadata-required.md +44 -0
  120. package/docs/rules/lifecycle-build-context-required.md +39 -0
  121. package/docs/rules/lifecycle-build-must-forward-context.md +50 -0
  122. package/docs/rules/no-cdk-api-above-floor.md +56 -0
  123. package/docs/rules/no-cjs-incompatible-syntax.md +48 -0
  124. package/docs/rules/no-realm-bound-instanceof.md +87 -0
  125. package/docs/rules/no-typescript-private-modifier.md +72 -0
  126. package/docs/rules/redeclared-prop-must-track-cdk-type.md +60 -0
  127. package/package.json +81 -0
@@ -0,0 +1,88 @@
1
+ /**
2
+ * The member's own name, for the message. A computed key has no name to give —
3
+ * `#[expr]` is not a thing — so the message falls back to a placeholder rather
4
+ * than naming whatever expression produced the key.
5
+ */
6
+ function memberName(key, computed) {
7
+ if (computed)
8
+ return "field";
9
+ if (key.type === "Identifier")
10
+ return key.name;
11
+ if (key.type === "Literal" && typeof key.value === "string")
12
+ return key.value;
13
+ return "field";
14
+ }
15
+ /** The bound name of a parameter, seeing through a default value. */
16
+ function parameterName(parameter) {
17
+ const bound = parameter.type === "AssignmentPattern" ? parameter.left : parameter;
18
+ return bound.type === "Identifier" ? bound.name : "field";
19
+ }
20
+ /**
21
+ * Bans the TypeScript `private` modifier in favour of ECMAScript private fields.
22
+ *
23
+ * The two are not interchangeable once a class is generic over its own members.
24
+ * A TypeScript `private` member is still part of the type, so it appears in
25
+ * `keyof T`, survives into a mapped type, and reaches the emitted `.d.ts` — where
26
+ * a consumer's compiler cannot name it and reports TS4094. A `#field` is not
27
+ * part of the type at all, so it never gets that far.
28
+ *
29
+ * A private constructor stays allowed: `#constructor` is not valid syntax, so
30
+ * there is nothing to prefer over it.
31
+ *
32
+ * See {@link https://github.com/laazyj/composureCDK/blob/main/packages/eslint-plugin/docs/rules/no-typescript-private-modifier.md | the rule documentation}.
33
+ */
34
+ export const rule = {
35
+ meta: {
36
+ type: "problem",
37
+ docs: {
38
+ description: "Ban the TypeScript `private` modifier in favour of ECMAScript `#` fields",
39
+ },
40
+ schema: [],
41
+ messages: {
42
+ property: "Use an ECMAScript private field (`#{{name}}`) instead of the TypeScript `private` " +
43
+ "modifier. A `private` member still appears in `keyof T` and is carried through mapped " +
44
+ "types into the emitted .d.ts, where it produces TS4094 for consumers.",
45
+ method: "Use an ECMAScript private method (`#{{name}}()`) instead of the TypeScript `private` " +
46
+ "modifier. A private constructor is the only permitted use, since `#constructor` is not " +
47
+ "valid syntax.",
48
+ parameterProperty: "A parameter property cannot be ECMAScript private. Declare the field as " +
49
+ "`readonly #{{name}}` and assign it in the constructor body.",
50
+ },
51
+ },
52
+ create(ctx) {
53
+ // Each handler reports on the member's own name rather than the whole
54
+ // declaration: the fix replaces the modifier and renames the key, so a
55
+ // narrow squiggle points at both.
56
+ return {
57
+ "PropertyDefinition[accessibility='private']"(node) {
58
+ ctx.report({
59
+ node: node.key,
60
+ messageId: "property",
61
+ data: { name: memberName(node.key, node.computed) },
62
+ });
63
+ },
64
+ "MethodDefinition[accessibility='private'][kind!='constructor']"(node) {
65
+ ctx.report({
66
+ node: node.key,
67
+ messageId: "method",
68
+ data: { name: memberName(node.key, node.computed) },
69
+ });
70
+ },
71
+ // `accessor x` is a distinct node from a plain field, and its `private`
72
+ // reaches `keyof T` the same way.
73
+ "AccessorProperty[accessibility='private']"(node) {
74
+ const { key, computed } = node;
75
+ ctx.report({ node: key, messageId: "property", data: { name: memberName(key, computed) } });
76
+ },
77
+ "TSParameterProperty[accessibility='private']"(node) {
78
+ const { parameter } = node;
79
+ ctx.report({
80
+ node: parameter,
81
+ messageId: "parameterProperty",
82
+ data: { name: parameterName(parameter) },
83
+ });
84
+ },
85
+ };
86
+ },
87
+ };
88
+ //# sourceMappingURL=no-typescript-private-modifier.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-typescript-private-modifier.js","sourceRoot":"","sources":["../../../src/rules/no-typescript-private-modifier.ts"],"names":[],"mappings":"AAgBA;;;;GAIG;AACH,SAAS,UAAU,CAAC,GAAS,EAAE,QAAiB;IAC9C,IAAI,QAAQ;QAAE,OAAO,OAAO,CAAC;IAC7B,IAAI,GAAG,CAAC,IAAI,KAAK,YAAY;QAAE,OAAO,GAAG,CAAC,IAAI,CAAC;IAC/C,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS,IAAI,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ;QAAE,OAAO,GAAG,CAAC,KAAK,CAAC;IAC9E,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,qEAAqE;AACrE,SAAS,aAAa,CAAC,SAAe;IACpC,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,KAAK,mBAAmB,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;IAClF,OAAO,KAAK,CAAC,IAAI,KAAK,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC;AAC5D,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,IAAI,GAAoB;IACnC,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,IAAI,EAAE;YACJ,WAAW,EAAE,0EAA0E;SACxF;QACD,MAAM,EAAE,EAAE;QACV,QAAQ,EAAE;YACR,QAAQ,EACN,oFAAoF;gBACpF,wFAAwF;gBACxF,uEAAuE;YACzE,MAAM,EACJ,uFAAuF;gBACvF,yFAAyF;gBACzF,eAAe;YACjB,iBAAiB,EACf,0EAA0E;gBAC1E,6DAA6D;SAChE;KACF;IACD,MAAM,CAAC,GAAG;QACR,sEAAsE;QACtE,uEAAuE;QACvE,kCAAkC;QAClC,OAAO;YACL,6CAA6C,CAAC,IAAwB;gBACpE,GAAG,CAAC,MAAM,CAAC;oBACT,IAAI,EAAE,IAAI,CAAC,GAAG;oBACd,SAAS,EAAE,UAAU;oBACrB,IAAI,EAAE,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE;iBACpD,CAAC,CAAC;YACL,CAAC;YACD,gEAAgE,CAAC,IAAsB;gBACrF,GAAG,CAAC,MAAM,CAAC;oBACT,IAAI,EAAE,IAAI,CAAC,GAAG;oBACd,SAAS,EAAE,QAAQ;oBACnB,IAAI,EAAE,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE;iBACpD,CAAC,CAAC;YACL,CAAC;YACD,wEAAwE;YACxE,kCAAkC;YAClC,2CAA2C,CAAC,IAAU;gBACpD,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,IAAuC,CAAC;gBAClE,GAAG,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,UAAU,CAAC,GAAG,EAAE,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC;YAC9F,CAAC;YACD,8CAA8C,CAAC,IAAU;gBACvD,MAAM,EAAE,SAAS,EAAE,GAAG,IAAwC,CAAC;gBAC/D,GAAG,CAAC,MAAM,CAAC;oBACT,IAAI,EAAE,SAAS;oBACf,SAAS,EAAE,mBAAmB;oBAC9B,IAAI,EAAE,EAAE,IAAI,EAAE,aAAa,CAAC,SAAS,CAAC,EAAE;iBACzC,CAAC,CAAC;YACL,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC"}
@@ -0,0 +1,17 @@
1
+ import type { Rule } from "eslint";
2
+ /**
3
+ * Flags a re-declared CDK prop that pins a named CDK interface inside
4
+ * `Resolvable<…>` rather than reading the type from CDK's own prop.
5
+ *
6
+ * A builder's props type is CDK's props type with a few keys lifted out and
7
+ * re-declared. Everything left inside the `Omit` keeps tracking whichever
8
+ * `aws-cdk-lib` the consumer installed; a key re-spelled with a named interface
9
+ * stops tracking it, freezing at whatever that interface meant when it was
10
+ * written. When CDK later widens that prop, the builder goes on rejecting values
11
+ * the construct it wraps now accepts — and it surfaces in the consumer's own
12
+ * compile, not in this package's.
13
+ *
14
+ * See {@link https://github.com/laazyj/composureCDK/blob/main/packages/eslint-plugin/docs/rules/redeclared-prop-must-track-cdk-type.md | the rule documentation}.
15
+ */
16
+ export declare const rule: Rule.RuleModule;
17
+ //# sourceMappingURL=redeclared-prop-must-track-cdk-type.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redeclared-prop-must-track-cdk-type.d.ts","sourceRoot":"","sources":["../../../src/rules/redeclared-prop-must-track-cdk-type.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAqFnC;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,IAAI,EAAE,IAAI,CAAC,UAiEvB,CAAC"}
@@ -0,0 +1,149 @@
1
+ import { isCdkSource } from "./lib/imports.js";
2
+ /** True for `@aws-cdk/aws-*-alpha` and friends, alongside `aws-cdk-lib`. */
3
+ function isCdkModule(source) {
4
+ return isCdkSource(source) || source.startsWith("@aws-cdk/");
5
+ }
6
+ /** The first type argument of a type reference or interface heritage, either spelling. */
7
+ function typeArgAt(node, index) {
8
+ const args = (node.typeArguments ?? node.typeParameters);
9
+ const params = args?.params ?? [];
10
+ return params.at(index);
11
+ }
12
+ /** The leftmost identifier of `Foo` or `ns.Foo`, or `undefined` for anything else. */
13
+ function rootName(typeName) {
14
+ let current = typeName;
15
+ while (current.type === "TSQualifiedName")
16
+ current = current.left;
17
+ return current.type === "Identifier" ? current.name : undefined;
18
+ }
19
+ /** The string keys named by `Omit`'s second argument, ignoring non-literal ones. */
20
+ function literalKeys(node) {
21
+ if (node === undefined)
22
+ return [];
23
+ if (node.type === "TSUnionType") {
24
+ return node.types.flatMap((t) => literalKeys(t));
25
+ }
26
+ if (node.type !== "TSLiteralType")
27
+ return [];
28
+ const literal = node.literal;
29
+ return typeof literal.value === "string" ? [literal.value] : [];
30
+ }
31
+ /** The keys an interface's `extends` clause omits from a base CDK props type. */
32
+ function omittedKeys(heritage) {
33
+ const keys = new Set();
34
+ for (const entry of heritage) {
35
+ const expression = entry.expression;
36
+ if (expression?.type !== "Identifier" || expression.name !== "Omit")
37
+ continue;
38
+ for (const key of literalKeys(typeArgAt(entry, 1)))
39
+ keys.add(key);
40
+ }
41
+ return keys;
42
+ }
43
+ /** Every `Resolvable<…>` reference nested anywhere inside a type annotation. */
44
+ function resolvableRefs(node, found = []) {
45
+ if (node === null || typeof node !== "object" || !("type" in node))
46
+ return found;
47
+ const typed = node;
48
+ if (typed.type === "TSTypeReference" && rootName(typed.typeName) === "Resolvable") {
49
+ found.push(typed);
50
+ }
51
+ for (const [key, value] of Object.entries(typed)) {
52
+ // `parent` points back up the tree — following it never terminates.
53
+ if (key === "parent")
54
+ continue;
55
+ if (Array.isArray(value))
56
+ for (const item of value)
57
+ resolvableRefs(item, found);
58
+ else
59
+ resolvableRefs(value, found);
60
+ }
61
+ return found;
62
+ }
63
+ /** The property name of a `key: T` signature, or `undefined` for a computed one. */
64
+ function propertyName(member) {
65
+ if (member.type !== "TSPropertySignature" || member.computed === true)
66
+ return undefined;
67
+ const key = member.key;
68
+ if (key.type === "Identifier")
69
+ return key.name;
70
+ if (key.type === "Literal" && typeof key.value === "string")
71
+ return key.value;
72
+ return undefined;
73
+ }
74
+ /**
75
+ * Flags a re-declared CDK prop that pins a named CDK interface inside
76
+ * `Resolvable<…>` rather than reading the type from CDK's own prop.
77
+ *
78
+ * A builder's props type is CDK's props type with a few keys lifted out and
79
+ * re-declared. Everything left inside the `Omit` keeps tracking whichever
80
+ * `aws-cdk-lib` the consumer installed; a key re-spelled with a named interface
81
+ * stops tracking it, freezing at whatever that interface meant when it was
82
+ * written. When CDK later widens that prop, the builder goes on rejecting values
83
+ * the construct it wraps now accepts — and it surfaces in the consumer's own
84
+ * compile, not in this package's.
85
+ *
86
+ * See {@link https://github.com/laazyj/composureCDK/blob/main/packages/eslint-plugin/docs/rules/redeclared-prop-must-track-cdk-type.md | the rule documentation}.
87
+ */
88
+ export const rule = {
89
+ meta: {
90
+ type: "problem",
91
+ docs: {
92
+ description: "A re-declared CDK prop must read its type from CDK's own prop, not pin an interface",
93
+ },
94
+ schema: [],
95
+ messages: {
96
+ pinnedType: "`{{prop}}` re-declares a prop omitted from `{{base}}`, but pins `{{pinned}}` — the " +
97
+ "builder stops accepting what CDK accepts as soon as CDK widens the prop. Read the type " +
98
+ 'from CDK\'s own prop instead: `Resolvable<NonNullable<{{base}}["{{prop}}"]>>`. ' +
99
+ "Keep the indexed access inline — extracting it to a named alias puts an unnameable " +
100
+ "type into the emitted declaration.",
101
+ },
102
+ },
103
+ create(ctx) {
104
+ const cdkTypeNames = new Set();
105
+ const listener = {
106
+ ImportDeclaration(node) {
107
+ if (typeof node.source.value !== "string" || !isCdkModule(node.source.value))
108
+ return;
109
+ for (const spec of node.specifiers) {
110
+ cdkTypeNames.add(spec.local.name);
111
+ }
112
+ },
113
+ };
114
+ listener.TSInterfaceDeclaration = (node) => {
115
+ const decl = node;
116
+ const heritage = decl.extends ?? [];
117
+ const omitted = omittedKeys(heritage);
118
+ if (omitted.size === 0)
119
+ return;
120
+ // Name the base the indexed access should read from — the first `Omit`'s
121
+ // own base, which is the CDK props type in every builder that has one.
122
+ const omit = heritage.find((entry) => entry.expression?.name === "Omit");
123
+ const omitBase = omit === undefined ? undefined : typeArgAt(omit, 0);
124
+ const base = (omitBase === undefined ? undefined : rootName(omitBase.typeName)) ??
125
+ "CdkProps";
126
+ const body = decl.body?.body ?? [];
127
+ for (const member of body) {
128
+ const prop = propertyName(member);
129
+ if (prop === undefined || !omitted.has(prop))
130
+ continue;
131
+ for (const resolvable of resolvableRefs(member.typeAnnotation)) {
132
+ const inner = typeArgAt(resolvable, 0);
133
+ if (inner?.type !== "TSTypeReference")
134
+ continue;
135
+ const pinned = rootName(inner.typeName);
136
+ if (pinned === undefined || !cdkTypeNames.has(pinned))
137
+ continue;
138
+ ctx.report({
139
+ node: inner,
140
+ messageId: "pinnedType",
141
+ data: { prop, base, pinned },
142
+ });
143
+ }
144
+ }
145
+ };
146
+ return listener;
147
+ },
148
+ };
149
+ //# sourceMappingURL=redeclared-prop-must-track-cdk-type.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redeclared-prop-must-track-cdk-type.js","sourceRoot":"","sources":["../../../src/rules/redeclared-prop-must-track-cdk-type.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ # composurecdk/builder-must-be-tagged
2
+
3
+ Flags `Builder()` or `IBuilder<…>` imported from `@composurecdk/core` in a library builder, where the tagged equivalents belong.
4
+
5
+ - **Preset:** `internal` (`error`)
6
+
7
+ ## Why
8
+
9
+ Tagging is a cross-cutting concern every deployable resource needs, and it is not something each builder should re-invent. `taggedBuilder` / `ITaggedBuilder` from `@composurecdk/cloudformation` wrap the core builder with the shared tagging surface, so a consumer gets the same `.tags(…)` API on every builder in the library rather than on whichever ones remembered.
10
+
11
+ `@composurecdk/core` stays CDK-agnostic and cannot provide this itself — tagging is a CloudFormation concern, so the tagged wrapper lives one layer up.
12
+
13
+ ## ❌ Incorrect
14
+
15
+ ```ts
16
+ import { Builder, type IBuilder } from "@composurecdk/core";
17
+
18
+ export function createBucketBuilder(): IBuilder<BucketBuilderProps, Bucket> {
19
+ return Builder(BucketBuilderImpl);
20
+ }
21
+ ```
22
+
23
+ ## ✅ Correct
24
+
25
+ ```ts
26
+ import { taggedBuilder, type ITaggedBuilder } from "@composurecdk/cloudformation";
27
+
28
+ export function createBucketBuilder(): ITaggedBuilder<BucketBuilderProps, Bucket> {
29
+ return taggedBuilder(BucketBuilderImpl);
30
+ }
31
+ ```
32
+
33
+ ## Escape hatch
34
+
35
+ Some CloudFormation resources have no `Tags` property at all — Route53 records, IAM `ManagedPolicy`, SNS `Subscription`, AWS Budgets. A builder wrapping one of those cannot be tagged, so disable the rule on the line and name the resource, keeping the exception visible in review:
36
+
37
+ ```ts
38
+ // eslint-disable-next-line composurecdk/builder-must-be-tagged -- AWS::Route53::RecordSet has no Tags property
39
+ return Builder(RecordSetBuilderImpl);
40
+ ```
41
+
42
+ The implementation of the tagged wrapper itself is exempt in this repo's root config — `packages/cloudformation/src/tagged-builder.ts` _is_ the tagged surface, so by definition it reaches for the core builder.
43
+
44
+ ## How it works
45
+
46
+ Syntactic. The rule looks for `Builder` / `IBuilder` bindings imported from `@composurecdk/core` and reports their use.
@@ -0,0 +1,50 @@
1
+ # composurecdk/builder-must-implement-copy-state
2
+
3
+ Flags a builder class holding private state with no `[COPY_STATE]` hook.
4
+
5
+ - **Preset:** `recommended` (`error`)
6
+ - **Decision:** [ADR-0005](https://github.com/laazyj/composureCDK/blob/main/docs/adr/0005-builder-copy.md)
7
+
8
+ ## Why
9
+
10
+ `.copy()` shallow-clones `props`. Anything a builder keeps _outside_ `props` — private fields — is not carried across unless `[COPY_STATE]` says so. Without the hook, `.copy()` silently drops that state, breaking both variant authoring and strategy hand-off, and it fails quietly: the copy looks fine and behaves differently.
11
+
12
+ [ADR-0005](https://github.com/laazyj/composureCDK/blob/main/docs/adr/0005-builder-copy.md) has the design.
13
+
14
+ ## ❌ Incorrect
15
+
16
+ ```ts
17
+ class BucketBuilderImpl {
18
+ #alarms: AlarmDefinition[] = [];
19
+ // no [COPY_STATE] — `.copy()` returns a builder with no alarms
20
+ }
21
+ ```
22
+
23
+ ## ✅ Correct
24
+
25
+ ```ts
26
+ class BucketBuilderImpl {
27
+ #alarms: AlarmDefinition[] = [];
28
+
29
+ [COPY_STATE](target: this): void {
30
+ target.#alarms = [...this.#alarms];
31
+ }
32
+ }
33
+ ```
34
+
35
+ ## Existence, not correctness
36
+
37
+ The rule checks only that the hook is **present**. A hook copying three of five fields passes. The companion test helper `assertCopyPreservesState` from `@composurecdk/core/testing` closes that gap on the test side — the two are meant to be used together.
38
+
39
+ ## Per-field opt-out
40
+
41
+ Annotate a field to exempt it, e.g. for cache-shaped state regenerated on each build. The justification after `--` is required:
42
+
43
+ ```ts
44
+ // @copy-state: ignore -- memoised per build, regenerated on the clone
45
+ #resolvedArn?: string;
46
+ ```
47
+
48
+ ## How it works
49
+
50
+ Syntactic. The rule identifies builder classes as those passed as the first argument to `Builder()` or `taggedBuilder()`, then checks for private fields against the presence of a `[COPY_STATE]` member.
@@ -0,0 +1,44 @@
1
+ # composurecdk/constraint-metadata-required
2
+
3
+ Requires every `stringConstraint({ … })` call to set a non-empty `name`, `allowed`, and `source`.
4
+
5
+ - **Preset:** `internal` (`error`)
6
+ - **Decision:** [ADR-0010](https://github.com/laazyj/composureCDK/blob/main/docs/adr/0010-aws-property-constraints.md)
7
+
8
+ ## Why
9
+
10
+ Those three fields are the whole value of a synth-time validation error. They name the property, list the allowed character set, and link the AWS documentation — turning "deployment failed" into something the author can act on at the call site.
11
+
12
+ The factory's type already makes the keys required, so the rule's real job is what the type checker cannot see: a present-but-empty literal such as `allowed: ""` compiles fine while silently degrading every message the constraint produces.
13
+
14
+ ## ❌ Incorrect
15
+
16
+ ```ts
17
+ const GROUP_DESCRIPTION = stringConstraint({
18
+ name: "EC2 SecurityGroup GroupDescription",
19
+ charClass: "a-zA-Z0-9 ._\\-:/()#,@\\[\\]+=&;{}!$*",
20
+ allowed: "",
21
+ source: "",
22
+ });
23
+ ```
24
+
25
+ ## ✅ Correct
26
+
27
+ ```ts
28
+ const GROUP_DESCRIPTION = stringConstraint({
29
+ name: "EC2 SecurityGroup GroupDescription",
30
+ charClass: "a-zA-Z0-9 ._\\-:/()#,@\\[\\]+=&;{}!$*",
31
+ allowed: "letters, digits, spaces and ._-:/()#,@[]+=&;{}!$*",
32
+ source:
33
+ "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-security-group.html",
34
+ });
35
+ ```
36
+
37
+ ## Not flagged
38
+
39
+ - **Non-literal values** — `allowed: SG_ALLOWED`. The contents are not statically knowable, so the rule leaves them alone.
40
+ - **Calls that spread another object** — skipped, to avoid false positives where the required field arrives via the spread.
41
+
42
+ ## How it works
43
+
44
+ Syntactic. The rule keys on the `stringConstraint` callee name, which is unique to the constraint-catalogue mechanism.
@@ -0,0 +1,39 @@
1
+ # composurecdk/lifecycle-build-context-required
2
+
3
+ Flags a `Lifecycle` class that uses `Resolvable<…>` but whose `build` method takes no `context` parameter.
4
+
5
+ - **Preset:** `recommended` (`error`)
6
+
7
+ ## Why
8
+
9
+ A builder holding a `Resolvable<…>` accepts refs at configuration time. Resolving one at build time needs the context — `resolve(value, context)`. With no `context` parameter the call receives `undefined`, and the ref throws "cannot be resolved" at synth.
10
+
11
+ This rule checks the **declaration**. Its call-site counterpart, [`lifecycle-build-must-forward-context`](lifecycle-build-must-forward-context.md), checks that a builder which delegates actually passes its context on — having the parameter is not enough.
12
+
13
+ ## ❌ Incorrect
14
+
15
+ ```ts
16
+ class TopicBuilderImpl implements Lifecycle<Topic> {
17
+ #props: { masterKey?: Resolvable<IKey> } = {};
18
+
19
+ build(scope: IConstruct, id: string): Topic {
20
+ // no `context` — resolving `masterKey` here cannot work
21
+ }
22
+ }
23
+ ```
24
+
25
+ ## ✅ Correct
26
+
27
+ ```ts
28
+ class TopicBuilderImpl implements Lifecycle<Topic> {
29
+ #props: { masterKey?: Resolvable<IKey> } = {};
30
+
31
+ build(scope: IConstruct, id: string, context?: BuildContext): Topic {
32
+ const masterKey = resolve(this.#props.masterKey, context);
33
+ }
34
+ }
35
+ ```
36
+
37
+ ## How it works
38
+
39
+ Syntactic. The rule keys on the identifier `Resolvable` appearing in the class body — a name unique to this codebase. Keying on `resolve(` instead would produce false positives on `Promise.resolve` and friends.
@@ -0,0 +1,50 @@
1
+ # composurecdk/lifecycle-build-must-forward-context
2
+
3
+ Flags a two-argument `builder.build(scope, id)` call in library source, which gives the sub-builder no context to resolve refs against.
4
+
5
+ - **Preset:** `libraryAuthor` (`error`)
6
+
7
+ ## Why
8
+
9
+ This is the call-site counterpart to [`lifecycle-build-context-required`](lifecycle-build-context-required.md), which checks the declaration. Accepting a `context` parameter is not enough: a builder that delegates to a sub-builder must pass its context on. When it does not, the sub-builder resolves refs against `{}`, and any `ref()` the caller supplied through a `configure` callback dies with "component not found in context".
10
+
11
+ The declaration rule cannot see this. It keys on `Resolvable<` in the builder's own class body, but in every real occurrence the resolvable lived one delegation away — in the _sub-builder's_ props — while the offending call sat in a free helper function (`resolveAccessLogs`, `resolveFlowLogs`, …), often in another file.
12
+
13
+ Checking the call site catches both failure modes at once: to satisfy the rule at the leaf you must thread `context` into the helper, which in turn forces the parameter onto the parent's `build`.
14
+
15
+ ## ❌ Incorrect
16
+
17
+ ```ts
18
+ function resolveAccessLogs(scope: IConstruct, props: Props): Bucket {
19
+ return props.logBucket.build(scope, "LogBucket");
20
+ }
21
+ ```
22
+
23
+ ## ✅ Correct
24
+
25
+ ```ts
26
+ function resolveAccessLogs(scope: IConstruct, props: Props, context?: BuildContext): Bucket {
27
+ return props.logBucket.build(scope, "LogBucket", context);
28
+ }
29
+ ```
30
+
31
+ ## Why exactly two arguments
32
+
33
+ `Lifecycle.build(scope, id, context?)` is always called with at least `scope` and `id`, so a two-argument call is the precise signature of the bug. Matching exactly two — rather than "fewer than three" — keeps the rule off unrelated `build()` methods that share the name, notably `StatementBuilder.build()` in `@composurecdk/iam`, which takes none.
34
+
35
+ A spread call such as `target.build(...args)` is one argument and is likewise skipped: that is the generic forwarding wrapper in `tagged-builder.ts`, which passes context through by construction.
36
+
37
+ ## Escape hatch
38
+
39
+ A root-level build genuinely has no context to forward — nothing composed it. Silence those explicitly, so the exception stays visible in review:
40
+
41
+ ```ts
42
+ // eslint-disable-next-line composurecdk/lifecycle-build-must-forward-context -- root build: the strategy callback receives only (scope, id)
43
+ stackBuilder.build(scope, id);
44
+ ```
45
+
46
+ Application entry points build at the root as a matter of course, so this repo's root config switches the rule off for `packages/examples/src`.
47
+
48
+ ## How it works
49
+
50
+ Deliberately syntactic, like every rule in this plugin. Type information would let it assert the receiver really is a `Lifecycle`, but the residual false-positive set is small and specific — root-level standalone builds, above — so the added cost is not yet worth paying.
@@ -0,0 +1,56 @@
1
+ # composurecdk/no-cdk-api-above-floor
2
+
3
+ Flags use of an `aws-cdk-lib` API newer than the package's supported peer-dependency floor.
4
+
5
+ - **Preset:** `internal` (`error`)
6
+ - **Decision:** [ADR-0008](https://github.com/laazyj/composureCDK/blob/main/docs/adr/0008-aws-cdk-lib-version-floors.md)
7
+
8
+ ## Why
9
+
10
+ Such calls compile fine — devDependencies track the latest CDK — but **throw at runtime** for a consumer on an older, still-supported version inside the declared `^2` peer range. Nothing else catches it: the type checker sees the newest CDK, and the unit suites run against it too.
11
+
12
+ [ADR-0008](https://github.com/laazyj/composureCDK/blob/main/docs/adr/0008-aws-cdk-lib-version-floors.md) covers how the floors are chosen and enforced.
13
+
14
+ ## ❌ Incorrect
15
+
16
+ ```ts
17
+ import { CfnAlarm } from "aws-cdk-lib/aws-cloudwatch";
18
+
19
+ if (CfnAlarm.isCfnAlarm(node)) {
20
+ // TypeError on aws-cdk-lib < 2.231.0
21
+ }
22
+ ```
23
+
24
+ ## ✅ Correct
25
+
26
+ ```ts
27
+ import { CfnResource } from "aws-cdk-lib";
28
+ import { CfnAlarm } from "aws-cdk-lib/aws-cloudwatch";
29
+
30
+ if (CfnResource.isCfnResource(node) && node.cfnResourceType === CfnAlarm.CFN_RESOURCE_TYPE_NAME) {
31
+ // what the static does internally, valid across the whole peer range
32
+ }
33
+ ```
34
+
35
+ `@composurecdk/cloudwatch` exposes this as an `isCfnAlarm` helper; see [ADR-0011](https://github.com/laazyj/composureCDK/blob/main/docs/adr/0011-cross-component-relationship-guards.md) for the pattern.
36
+
37
+ ## The ban list
38
+
39
+ Currently one entry: the per-resource `Cfn<Resource>.isCfn<Resource>` L1 static type guards, which `aws-cdk-lib` first shipped on every generated L1 in **2.231.0** (verified by installing real versions — 2.230.0 lacks them). See issue #146.
40
+
41
+ `isCfnResource` and `isCfnElement` are explicitly allowed: they are core, predate the floor, and `isCfnResource` is the portable replacement.
42
+
43
+ The list is expected to grow as floors are pinned and lowered ahead of 1.0.0. Add an entry whenever an API turns out to postdate the floor.
44
+
45
+ ## Known gaps
46
+
47
+ Both uncommon in this ESM source:
48
+
49
+ - Computed bracket access — `CfnAlarm["isCfnAlarm"](x)`.
50
+ - `import = require()` bindings.
51
+
52
+ ## How it works
53
+
54
+ Matched by the accessed **member name** rather than the owning class identifier, so it holds regardless of how the class is imported or aliased — `CfnAlarm.isCfnAlarm`, `cw.CfnAlarm.isCfnAlarm`, `cdk.aws_cloudwatch.CfnAlarm.isCfnAlarm` all match.
55
+
56
+ Resolution runs through ESLint's scope manager, so a local shadowing the import name (a parameter called `CfnAlarm`, say) is correctly treated as a non-CDK binding. TS-only wrappers (`as`, `satisfies`, `!`, angle-bracket assertion) are unwrapped so they cannot smuggle a call past the rule. A call in the chain — `Stack.of(x).isCfnY()` — breaks the root link, since that reads a runtime value rather than the import.
@@ -0,0 +1,48 @@
1
+ # composurecdk/no-cjs-incompatible-syntax
2
+
3
+ Flags syntax in library `src/` that has no CommonJS emit: `import.meta`, top-level `await`, and top-level `for await…of`.
4
+
5
+ - **Preset:** `dualPublishing` (`error`)
6
+ - **Decision:** [ADR-0007](https://github.com/laazyj/composureCDK/blob/main/docs/adr/0007-dual-esm-cjs-publishing.md)
7
+
8
+ ## Why
9
+
10
+ Every `@composurecdk/*` package is dual-published, ESM and CommonJS. All three constructs are valid ESM with no CJS equivalent, so `tsc` — and `tshy`'s CommonJS dialect — errors on them.
11
+
12
+ Catching them at lint time gives an in-editor error immediately, rather than waiting for the per-dialect compile to fail.
13
+
14
+ ## ❌ Incorrect
15
+
16
+ ```ts
17
+ const here = import.meta.url;
18
+
19
+ const config = await loadConfig();
20
+
21
+ for await (const chunk of stream) {
22
+ // …
23
+ }
24
+ ```
25
+
26
+ ## ✅ Correct
27
+
28
+ ```ts
29
+ import { fileURLToPath } from "node:url";
30
+
31
+ async function init(): Promise<Config> {
32
+ const config = await loadConfig();
33
+ for await (const chunk of stream) {
34
+ // …
35
+ }
36
+ return config;
37
+ }
38
+ ```
39
+
40
+ Move the `await` inside an async function. Where a module genuinely needs its own location, take it from a parameter or a caller-supplied value rather than `import.meta`.
41
+
42
+ ## Scope
43
+
44
+ Library `src/` only. Test files are never published, so they may use all three freely — this repo's root config applies the preset to `packages/*/src/**/*.ts`.
45
+
46
+ ## How it works
47
+
48
+ Syntactic. `import.meta` is matched as a `MetaProperty`; `await` and `for await…of` are reported only when no enclosing function declaration, function expression or arrow function is found in the ancestor chain — that is, only at the top level.