@intentius/chant 0.0.1
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/README.md +365 -0
- package/package.json +22 -0
- package/src/attrref.test.ts +148 -0
- package/src/attrref.ts +50 -0
- package/src/barrel.test.ts +157 -0
- package/src/barrel.ts +101 -0
- package/src/bench.test.ts +227 -0
- package/src/build.test.ts +437 -0
- package/src/build.ts +425 -0
- package/src/builder.test.ts +312 -0
- package/src/builder.ts +56 -0
- package/src/child-project.ts +44 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/README.md +26 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +14 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/package.json +16 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/index.mdx +8 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +7 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/tsconfig.json +10 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/examples/getting-started/.gitkeep +0 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/justfile +26 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +29 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/docs.ts +25 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate-cli.ts +8 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate.ts +74 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/naming.ts +33 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/package.ts +25 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/rollback.ts +45 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/coverage.ts +11 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/generated/.gitkeep +0 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/generator.ts +10 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/parser.ts +10 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/index.ts +9 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/rules/index.ts +1 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/rules/sample.ts +18 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lsp/completions.ts +14 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lsp/hover.ts +14 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +110 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/serializer.ts +24 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/spec/fetch.ts +21 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/spec/parse.ts +25 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate-cli.ts +4 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate.ts +24 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/tsconfig.json +10 -0
- package/src/cli/commands/__fixtures__/sample-rule.ts +11 -0
- package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +222 -0
- package/src/cli/commands/build.test.ts +149 -0
- package/src/cli/commands/build.ts +344 -0
- package/src/cli/commands/diff.test.ts +148 -0
- package/src/cli/commands/diff.ts +221 -0
- package/src/cli/commands/doctor.test.ts +239 -0
- package/src/cli/commands/doctor.ts +224 -0
- package/src/cli/commands/import.test.ts +379 -0
- package/src/cli/commands/import.ts +335 -0
- package/src/cli/commands/init-lexicon.test.ts +297 -0
- package/src/cli/commands/init-lexicon.ts +993 -0
- package/src/cli/commands/init.test.ts +317 -0
- package/src/cli/commands/init.ts +505 -0
- package/src/cli/commands/licenses.ts +165 -0
- package/src/cli/commands/lint.test.ts +332 -0
- package/src/cli/commands/lint.ts +408 -0
- package/src/cli/commands/list.test.ts +100 -0
- package/src/cli/commands/list.ts +108 -0
- package/src/cli/commands/update.test.ts +38 -0
- package/src/cli/commands/update.ts +207 -0
- package/src/cli/conflict-check.test.ts +255 -0
- package/src/cli/conflict-check.ts +89 -0
- package/src/cli/debug.ts +8 -0
- package/src/cli/format.test.ts +140 -0
- package/src/cli/format.ts +133 -0
- package/src/cli/handlers/build.ts +58 -0
- package/src/cli/handlers/dev.ts +38 -0
- package/src/cli/handlers/init.ts +46 -0
- package/src/cli/handlers/lint.ts +36 -0
- package/src/cli/handlers/misc.ts +57 -0
- package/src/cli/handlers/serve.ts +26 -0
- package/src/cli/index.ts +3 -0
- package/src/cli/lsp/capabilities.ts +46 -0
- package/src/cli/lsp/diagnostics.ts +52 -0
- package/src/cli/lsp/server.test.ts +618 -0
- package/src/cli/lsp/server.ts +393 -0
- package/src/cli/main.test.ts +257 -0
- package/src/cli/main.ts +224 -0
- package/src/cli/mcp/resources/context.ts +59 -0
- package/src/cli/mcp/server.test.ts +747 -0
- package/src/cli/mcp/server.ts +402 -0
- package/src/cli/mcp/tools/build.ts +117 -0
- package/src/cli/mcp/tools/import.ts +48 -0
- package/src/cli/mcp/tools/lint.ts +45 -0
- package/src/cli/plugins.test.ts +31 -0
- package/src/cli/plugins.ts +94 -0
- package/src/cli/registry.ts +73 -0
- package/src/cli/reporters/stylish.test.ts +282 -0
- package/src/cli/reporters/stylish.ts +186 -0
- package/src/cli/watch.test.ts +81 -0
- package/src/cli/watch.ts +101 -0
- package/src/codegen/case.test.ts +30 -0
- package/src/codegen/case.ts +11 -0
- package/src/codegen/coverage.ts +167 -0
- package/src/codegen/docs.ts +634 -0
- package/src/codegen/fetch.test.ts +119 -0
- package/src/codegen/fetch.ts +261 -0
- package/src/codegen/generate-registry.test.ts +118 -0
- package/src/codegen/generate-registry.ts +107 -0
- package/src/codegen/generate-runtime-index.test.ts +81 -0
- package/src/codegen/generate-runtime-index.ts +99 -0
- package/src/codegen/generate-typescript.test.ts +146 -0
- package/src/codegen/generate-typescript.ts +161 -0
- package/src/codegen/generate.ts +206 -0
- package/src/codegen/json-patch.test.ts +113 -0
- package/src/codegen/json-patch.ts +151 -0
- package/src/codegen/json-schema.test.ts +196 -0
- package/src/codegen/json-schema.ts +209 -0
- package/src/codegen/naming.ts +201 -0
- package/src/codegen/package.ts +161 -0
- package/src/codegen/rollback.test.ts +92 -0
- package/src/codegen/rollback.ts +115 -0
- package/src/codegen/topo-sort.test.ts +69 -0
- package/src/codegen/topo-sort.ts +46 -0
- package/src/codegen/typecheck.test.ts +37 -0
- package/src/codegen/typecheck.ts +74 -0
- package/src/codegen/validate.test.ts +86 -0
- package/src/codegen/validate.ts +143 -0
- package/src/composite.test.ts +426 -0
- package/src/composite.ts +243 -0
- package/src/config.test.ts +91 -0
- package/src/config.ts +87 -0
- package/src/declarable.test.ts +160 -0
- package/src/declarable.ts +47 -0
- package/src/detectLexicon.test.ts +236 -0
- package/src/detectLexicon.ts +37 -0
- package/src/discovery/cache.test.ts +78 -0
- package/src/discovery/cache.ts +86 -0
- package/src/discovery/collect.test.ts +269 -0
- package/src/discovery/collect.ts +51 -0
- package/src/discovery/cycles.test.ts +238 -0
- package/src/discovery/cycles.ts +107 -0
- package/src/discovery/files.test.ts +154 -0
- package/src/discovery/files.ts +61 -0
- package/src/discovery/graph.test.ts +476 -0
- package/src/discovery/graph.ts +150 -0
- package/src/discovery/import.test.ts +199 -0
- package/src/discovery/import.ts +20 -0
- package/src/discovery/index.test.ts +272 -0
- package/src/discovery/index.ts +132 -0
- package/src/discovery/resolve.test.ts +267 -0
- package/src/discovery/resolve.ts +54 -0
- package/src/errors.test.ts +138 -0
- package/src/errors.ts +86 -0
- package/src/import/base-parser.test.ts +67 -0
- package/src/import/base-parser.ts +48 -0
- package/src/import/generator.ts +21 -0
- package/src/import/ir-utils.test.ts +103 -0
- package/src/import/ir-utils.ts +87 -0
- package/src/import/parser.ts +41 -0
- package/src/index.ts +60 -0
- package/src/intrinsic-interpolation.test.ts +91 -0
- package/src/intrinsic-interpolation.ts +89 -0
- package/src/intrinsic.test.ts +69 -0
- package/src/intrinsic.ts +43 -0
- package/src/lexicon-integrity.test.ts +94 -0
- package/src/lexicon-integrity.ts +69 -0
- package/src/lexicon-manifest.test.ts +101 -0
- package/src/lexicon-manifest.ts +71 -0
- package/src/lexicon-output.test.ts +182 -0
- package/src/lexicon-output.ts +82 -0
- package/src/lexicon-schema.test.ts +239 -0
- package/src/lexicon-schema.ts +144 -0
- package/src/lexicon.ts +212 -0
- package/src/lint/config-overrides.test.ts +254 -0
- package/src/lint/config.test.ts +644 -0
- package/src/lint/config.ts +375 -0
- package/src/lint/declarative.test.ts +256 -0
- package/src/lint/declarative.ts +187 -0
- package/src/lint/engine.test.ts +465 -0
- package/src/lint/engine.ts +172 -0
- package/src/lint/named-checks.test.ts +37 -0
- package/src/lint/named-checks.ts +33 -0
- package/src/lint/parser.test.ts +129 -0
- package/src/lint/parser.ts +42 -0
- package/src/lint/post-synth.test.ts +113 -0
- package/src/lint/post-synth.ts +76 -0
- package/src/lint/presets/relaxed.json +19 -0
- package/src/lint/presets/strict.json +19 -0
- package/src/lint/rule-loader.test.ts +67 -0
- package/src/lint/rule-loader.ts +67 -0
- package/src/lint/rule-options.test.ts +141 -0
- package/src/lint/rule.test.ts +196 -0
- package/src/lint/rule.ts +98 -0
- package/src/lint/rules/barrel-import-style.test.ts +80 -0
- package/src/lint/rules/barrel-import-style.ts +59 -0
- package/src/lint/rules/composite-scope.ts +55 -0
- package/src/lint/rules/cor017-composite-name-match.test.ts +107 -0
- package/src/lint/rules/cor017-composite-name-match.ts +108 -0
- package/src/lint/rules/cor018-composite-prefer-lexicon-type.test.ts +172 -0
- package/src/lint/rules/cor018-composite-prefer-lexicon-type.ts +167 -0
- package/src/lint/rules/declarable-naming-convention.test.ts +69 -0
- package/src/lint/rules/declarable-naming-convention.ts +70 -0
- package/src/lint/rules/enforce-barrel-import.test.ts +169 -0
- package/src/lint/rules/enforce-barrel-import.ts +81 -0
- package/src/lint/rules/enforce-barrel-ref.test.ts +114 -0
- package/src/lint/rules/enforce-barrel-ref.ts +75 -0
- package/src/lint/rules/evl001-non-literal-expression.test.ts +158 -0
- package/src/lint/rules/evl001-non-literal-expression.ts +149 -0
- package/src/lint/rules/evl002-control-flow-resource.test.ts +110 -0
- package/src/lint/rules/evl002-control-flow-resource.ts +61 -0
- package/src/lint/rules/evl003-dynamic-property-access.test.ts +63 -0
- package/src/lint/rules/evl003-dynamic-property-access.ts +41 -0
- package/src/lint/rules/evl004-spread-non-const.test.ts +130 -0
- package/src/lint/rules/evl004-spread-non-const.ts +111 -0
- package/src/lint/rules/evl005-resource-block-body.test.ts +59 -0
- package/src/lint/rules/evl005-resource-block-body.ts +49 -0
- package/src/lint/rules/evl006-barrel-usage.test.ts +63 -0
- package/src/lint/rules/evl006-barrel-usage.ts +95 -0
- package/src/lint/rules/evl007-invalid-siblings.test.ts +87 -0
- package/src/lint/rules/evl007-invalid-siblings.ts +139 -0
- package/src/lint/rules/evl008-unresolvable-barrel-ref.test.ts +118 -0
- package/src/lint/rules/evl008-unresolvable-barrel-ref.ts +140 -0
- package/src/lint/rules/evl009-composite-no-constant.test.ts +162 -0
- package/src/lint/rules/evl009-composite-no-constant.ts +171 -0
- package/src/lint/rules/evl010-composite-no-transform.test.ts +121 -0
- package/src/lint/rules/evl010-composite-no-transform.ts +69 -0
- package/src/lint/rules/export-required.test.ts +213 -0
- package/src/lint/rules/export-required.ts +158 -0
- package/src/lint/rules/file-declarable-limit.test.ts +148 -0
- package/src/lint/rules/file-declarable-limit.ts +96 -0
- package/src/lint/rules/flat-declarations.test.ts +210 -0
- package/src/lint/rules/flat-declarations.ts +70 -0
- package/src/lint/rules/index.ts +99 -0
- package/src/lint/rules/no-cyclic-declarable-ref.test.ts +135 -0
- package/src/lint/rules/no-cyclic-declarable-ref.ts +178 -0
- package/src/lint/rules/no-redundant-type-import.test.ts +129 -0
- package/src/lint/rules/no-redundant-type-import.ts +85 -0
- package/src/lint/rules/no-redundant-value-cast.test.ts +51 -0
- package/src/lint/rules/no-redundant-value-cast.ts +46 -0
- package/src/lint/rules/no-string-ref.test.ts +100 -0
- package/src/lint/rules/no-string-ref.ts +66 -0
- package/src/lint/rules/no-unused-declarable-import.test.ts +74 -0
- package/src/lint/rules/no-unused-declarable-import.ts +103 -0
- package/src/lint/rules/no-unused-declarable.test.ts +134 -0
- package/src/lint/rules/no-unused-declarable.ts +118 -0
- package/src/lint/rules/prefer-namespace-import.test.ts +102 -0
- package/src/lint/rules/prefer-namespace-import.ts +63 -0
- package/src/lint/rules/single-concern-file.test.ts +156 -0
- package/src/lint/rules/single-concern-file.ts +98 -0
- package/src/lint/rules/stale-barrel-types.ts +60 -0
- package/src/lint/selectors.test.ts +113 -0
- package/src/lint/selectors.ts +188 -0
- package/src/lsp/lexicon-providers.ts +191 -0
- package/src/lsp/types.ts +79 -0
- package/src/mcp/types.ts +22 -0
- package/src/project/scan.test.ts +178 -0
- package/src/project/scan.ts +182 -0
- package/src/project/sync.test.ts +87 -0
- package/src/project/sync.ts +46 -0
- package/src/project-validation.test.ts +64 -0
- package/src/project-validation.ts +79 -0
- package/src/pseudo-parameter.test.ts +39 -0
- package/src/pseudo-parameter.ts +47 -0
- package/src/runtime.ts +68 -0
- package/src/serializer-walker.test.ts +124 -0
- package/src/serializer-walker.ts +83 -0
- package/src/serializer.ts +42 -0
- package/src/sort.test.ts +290 -0
- package/src/sort.ts +58 -0
- package/src/stack-output.ts +82 -0
- package/src/types.test.ts +307 -0
- package/src/types.ts +46 -0
- package/src/utils.test.ts +195 -0
- package/src/utils.ts +46 -0
- package/src/validation.test.ts +308 -0
- package/src/validation.ts +50 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import type { LintRule, LintContext, LintDiagnostic } from "../rule";
|
|
3
|
+
import { getCompositeFactory } from "./composite-scope";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* EVL009: No extractable constants inside Composite factory
|
|
7
|
+
*
|
|
8
|
+
* Composite factories should only reference props, sibling members, and barrel refs.
|
|
9
|
+
* Object literals and arrays-of-objects that don't reference any of these are
|
|
10
|
+
* extractable constants that belong in a separate file (e.g. defaults.ts).
|
|
11
|
+
*
|
|
12
|
+
* Triggers on: assumeRolePolicyDocument: { Version: "2012-10-17", Statement: [...] }
|
|
13
|
+
* Triggers on: managedPolicyArns: ["arn:aws:iam::aws:policy/..."]
|
|
14
|
+
* OK: assumeRolePolicyDocument: _.$.lambdaTrustPolicy
|
|
15
|
+
* OK: policies: props.policies
|
|
16
|
+
* OK: role: role.arn
|
|
17
|
+
* OK: action: "lambda:InvokeFunction" (simple string literal — not an object)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check whether any leaf node in the subtree references:
|
|
22
|
+
* - The props parameter identifier
|
|
23
|
+
* - A barrel ref (_.$ or $)
|
|
24
|
+
* - A local variable declared in the composite factory body
|
|
25
|
+
*/
|
|
26
|
+
function referencesPropsOrMembers(
|
|
27
|
+
node: ts.Node,
|
|
28
|
+
propsName: string,
|
|
29
|
+
localNames: Set<string>,
|
|
30
|
+
): boolean {
|
|
31
|
+
if (ts.isIdentifier(node)) {
|
|
32
|
+
// Direct props reference
|
|
33
|
+
if (node.text === propsName) return true;
|
|
34
|
+
// Local variable reference (sibling member)
|
|
35
|
+
if (localNames.has(node.text)) return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Barrel ref: _.$.something or $.something
|
|
39
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
40
|
+
if (isBarrelRef(node)) return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let found = false;
|
|
44
|
+
ts.forEachChild(node, (child) => {
|
|
45
|
+
if (!found && referencesPropsOrMembers(child, propsName, localNames)) {
|
|
46
|
+
found = true;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
return found;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isBarrelRef(node: ts.PropertyAccessExpression): boolean {
|
|
53
|
+
// _.$.x or $.x
|
|
54
|
+
const expr = node.expression;
|
|
55
|
+
if (ts.isIdentifier(expr) && expr.text === "$") return true;
|
|
56
|
+
if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.name) && expr.name.text === "$") {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if a value is an extractable constant:
|
|
64
|
+
* - Object literal (with any nesting)
|
|
65
|
+
* - Array literal containing object literals
|
|
66
|
+
* Simple primitives (string, number, boolean) are allowed inline.
|
|
67
|
+
*/
|
|
68
|
+
function isExtractableShape(node: ts.Node): boolean {
|
|
69
|
+
if (ts.isObjectLiteralExpression(node)) return true;
|
|
70
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
71
|
+
return node.elements.some(
|
|
72
|
+
(el) => ts.isObjectLiteralExpression(el) || ts.isArrayLiteralExpression(el),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function checkComposite(
|
|
79
|
+
call: ts.CallExpression,
|
|
80
|
+
context: LintContext,
|
|
81
|
+
diagnostics: LintDiagnostic[],
|
|
82
|
+
): void {
|
|
83
|
+
const factory = call.arguments[0];
|
|
84
|
+
if (!factory || (!ts.isArrowFunction(factory) && !ts.isFunctionExpression(factory))) return;
|
|
85
|
+
|
|
86
|
+
// Get props parameter name
|
|
87
|
+
const propsParam = factory.parameters[0];
|
|
88
|
+
const propsName = propsParam && ts.isIdentifier(propsParam.name) ? propsParam.name.text : "";
|
|
89
|
+
|
|
90
|
+
// Collect local variable names declared in the factory body
|
|
91
|
+
const localNames = new Set<string>();
|
|
92
|
+
const body = factory.body;
|
|
93
|
+
if (ts.isBlock(body)) {
|
|
94
|
+
for (const stmt of body.statements) {
|
|
95
|
+
if (ts.isVariableStatement(stmt)) {
|
|
96
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
97
|
+
if (ts.isIdentifier(decl.name)) {
|
|
98
|
+
localNames.add(decl.name.text);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Walk all NewExpression nodes inside the factory body
|
|
106
|
+
checkForConstants(body, context, diagnostics, propsName, localNames);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function checkForConstants(
|
|
110
|
+
node: ts.Node,
|
|
111
|
+
context: LintContext,
|
|
112
|
+
diagnostics: LintDiagnostic[],
|
|
113
|
+
propsName: string,
|
|
114
|
+
localNames: Set<string>,
|
|
115
|
+
): void {
|
|
116
|
+
if (ts.isNewExpression(node) && node.arguments && node.arguments.length > 0) {
|
|
117
|
+
const firstArg = node.arguments[0];
|
|
118
|
+
if (ts.isObjectLiteralExpression(firstArg)) {
|
|
119
|
+
for (const prop of firstArg.properties) {
|
|
120
|
+
if (!ts.isPropertyAssignment(prop)) continue;
|
|
121
|
+
const value = prop.initializer;
|
|
122
|
+
|
|
123
|
+
if (isExtractableShape(value) && !referencesPropsOrMembers(value, propsName, localNames)) {
|
|
124
|
+
const propName = ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)
|
|
125
|
+
? (ts.isIdentifier(prop.name) ? prop.name.text : prop.name.text)
|
|
126
|
+
: "<computed>";
|
|
127
|
+
|
|
128
|
+
const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(
|
|
129
|
+
value.getStart(context.sourceFile),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
diagnostics.push({
|
|
133
|
+
file: context.filePath,
|
|
134
|
+
line: line + 1,
|
|
135
|
+
column: character + 1,
|
|
136
|
+
ruleId: "EVL009",
|
|
137
|
+
severity: "warning",
|
|
138
|
+
message: `Extractable constant in Composite factory property "${propName}" — move to a separate file and reference via _.$.name`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
ts.forEachChild(node, (child) =>
|
|
146
|
+
checkForConstants(child, context, diagnostics, propsName, localNames),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function checkNode(node: ts.Node, context: LintContext, diagnostics: LintDiagnostic[]): void {
|
|
151
|
+
if (ts.isCallExpression(node)) {
|
|
152
|
+
const factory = getCompositeFactory(node);
|
|
153
|
+
if (factory) {
|
|
154
|
+
checkComposite(node, context, diagnostics);
|
|
155
|
+
return; // Don't recurse further — checkComposite handles it
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
ts.forEachChild(node, (child) => checkNode(child, context, diagnostics));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const evl009CompositeNoConstantRule: LintRule = {
|
|
163
|
+
id: "EVL009",
|
|
164
|
+
severity: "warning",
|
|
165
|
+
category: "style",
|
|
166
|
+
check(context: LintContext): LintDiagnostic[] {
|
|
167
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
168
|
+
checkNode(context.sourceFile, context, diagnostics);
|
|
169
|
+
return diagnostics;
|
|
170
|
+
},
|
|
171
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import * as ts from "typescript";
|
|
3
|
+
import { evl010CompositeNoTransformRule } from "./evl010-composite-no-transform";
|
|
4
|
+
import type { LintContext } from "../rule";
|
|
5
|
+
|
|
6
|
+
function createContext(code: string, filePath = "test.ts"): LintContext {
|
|
7
|
+
const sourceFile = ts.createSourceFile(filePath, code, ts.ScriptTarget.Latest, true);
|
|
8
|
+
return { sourceFile, entities: [], filePath, lexicon: undefined };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe("EVL010: composite-no-transform", () => {
|
|
12
|
+
test("rule metadata", () => {
|
|
13
|
+
expect(evl010CompositeNoTransformRule.id).toBe("EVL010");
|
|
14
|
+
expect(evl010CompositeNoTransformRule.severity).toBe("warning");
|
|
15
|
+
expect(evl010CompositeNoTransformRule.category).toBe("style");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("flags .map() inside Composite factory", () => {
|
|
19
|
+
const ctx = createContext(`
|
|
20
|
+
const MyComp = Composite((props) => {
|
|
21
|
+
const role = new Role({
|
|
22
|
+
policies: props.items.map(x => new Policy(x)),
|
|
23
|
+
});
|
|
24
|
+
return { role };
|
|
25
|
+
}, "MyComp");
|
|
26
|
+
`);
|
|
27
|
+
const diags = evl010CompositeNoTransformRule.check(ctx);
|
|
28
|
+
expect(diags).toHaveLength(1);
|
|
29
|
+
expect(diags[0].ruleId).toBe("EVL010");
|
|
30
|
+
expect(diags[0].message).toContain(".map()");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("flags .filter() inside Composite factory", () => {
|
|
34
|
+
const ctx = createContext(`
|
|
35
|
+
const MyComp = Composite((props) => {
|
|
36
|
+
const role = new Role({
|
|
37
|
+
policies: props.items.filter(Boolean),
|
|
38
|
+
});
|
|
39
|
+
return { role };
|
|
40
|
+
}, "MyComp");
|
|
41
|
+
`);
|
|
42
|
+
const diags = evl010CompositeNoTransformRule.check(ctx);
|
|
43
|
+
expect(diags).toHaveLength(1);
|
|
44
|
+
expect(diags[0].message).toContain(".filter()");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("flags .reduce() inside Composite factory", () => {
|
|
48
|
+
const ctx = createContext(`
|
|
49
|
+
const MyComp = _.Composite((props) => {
|
|
50
|
+
const tags = Object.keys(props.env).reduce((acc, k) => {
|
|
51
|
+
acc[k] = props.env[k];
|
|
52
|
+
return acc;
|
|
53
|
+
}, {});
|
|
54
|
+
const bucket = new Bucket({ tags });
|
|
55
|
+
return { bucket };
|
|
56
|
+
}, "MyComp");
|
|
57
|
+
`);
|
|
58
|
+
const diags = evl010CompositeNoTransformRule.check(ctx);
|
|
59
|
+
expect(diags).toHaveLength(1);
|
|
60
|
+
expect(diags[0].message).toContain(".reduce()");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("flags .flatMap() inside Composite factory", () => {
|
|
64
|
+
const ctx = createContext(`
|
|
65
|
+
const MyComp = Composite((props) => {
|
|
66
|
+
const role = new Role({
|
|
67
|
+
policies: props.groups.flatMap(g => g.policies),
|
|
68
|
+
});
|
|
69
|
+
return { role };
|
|
70
|
+
}, "MyComp");
|
|
71
|
+
`);
|
|
72
|
+
const diags = evl010CompositeNoTransformRule.check(ctx);
|
|
73
|
+
expect(diags).toHaveLength(1);
|
|
74
|
+
expect(diags[0].message).toContain(".flatMap()");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("does not flag .map() outside Composite factory", () => {
|
|
78
|
+
const ctx = createContext(`
|
|
79
|
+
const items = data.map(x => new Thing(x));
|
|
80
|
+
`);
|
|
81
|
+
expect(evl010CompositeNoTransformRule.check(ctx)).toHaveLength(0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("does not flag props pass-through", () => {
|
|
85
|
+
const ctx = createContext(`
|
|
86
|
+
const MyComp = Composite((props) => {
|
|
87
|
+
const role = new Role({
|
|
88
|
+
policies: props.policies,
|
|
89
|
+
});
|
|
90
|
+
return { role };
|
|
91
|
+
}, "MyComp");
|
|
92
|
+
`);
|
|
93
|
+
expect(evl010CompositeNoTransformRule.check(ctx)).toHaveLength(0);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("does not flag array literal", () => {
|
|
97
|
+
const ctx = createContext(`
|
|
98
|
+
const MyComp = Composite((props) => {
|
|
99
|
+
const role = new Role({
|
|
100
|
+
managedPolicyArns: [props.arn],
|
|
101
|
+
});
|
|
102
|
+
return { role };
|
|
103
|
+
}, "MyComp");
|
|
104
|
+
`);
|
|
105
|
+
expect(evl010CompositeNoTransformRule.check(ctx)).toHaveLength(0);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("flags multiple transforms", () => {
|
|
109
|
+
const ctx = createContext(`
|
|
110
|
+
const MyComp = Composite((props) => {
|
|
111
|
+
const role = new Role({
|
|
112
|
+
policies: props.items.map(x => new Policy(x)),
|
|
113
|
+
tags: props.tags.filter(t => t.active),
|
|
114
|
+
});
|
|
115
|
+
return { role };
|
|
116
|
+
}, "MyComp");
|
|
117
|
+
`);
|
|
118
|
+
const diags = evl010CompositeNoTransformRule.check(ctx);
|
|
119
|
+
expect(diags).toHaveLength(2);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import type { LintRule, LintContext, LintDiagnostic } from "../rule";
|
|
3
|
+
import { isInsideCompositeFactory } from "./composite-scope";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* EVL010: No data transformation inside Composite factory
|
|
7
|
+
*
|
|
8
|
+
* Composite factories should only compose resources from props, not transform data.
|
|
9
|
+
* Data transformation (map, filter, reduce, etc.) should happen before passing props.
|
|
10
|
+
*
|
|
11
|
+
* Triggers on: props.items.map(x => new Thing(x))
|
|
12
|
+
* Triggers on: props.list.filter(Boolean)
|
|
13
|
+
* Triggers on: Object.keys(props.env).reduce(...)
|
|
14
|
+
* OK: props.policies (pass-through)
|
|
15
|
+
* OK: [a, b] (array literal)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const TRANSFORM_METHODS = new Set([
|
|
19
|
+
"map",
|
|
20
|
+
"filter",
|
|
21
|
+
"reduce",
|
|
22
|
+
"flatMap",
|
|
23
|
+
"forEach",
|
|
24
|
+
"find",
|
|
25
|
+
"some",
|
|
26
|
+
"every",
|
|
27
|
+
"sort",
|
|
28
|
+
"reverse",
|
|
29
|
+
"splice",
|
|
30
|
+
"slice",
|
|
31
|
+
"concat",
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
function checkNode(node: ts.Node, context: LintContext, diagnostics: LintDiagnostic[]): void {
|
|
35
|
+
if (
|
|
36
|
+
ts.isCallExpression(node) &&
|
|
37
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
38
|
+
isInsideCompositeFactory(node)
|
|
39
|
+
) {
|
|
40
|
+
const methodName = node.expression.name.text;
|
|
41
|
+
if (TRANSFORM_METHODS.has(methodName)) {
|
|
42
|
+
const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(
|
|
43
|
+
node.expression.name.getStart(context.sourceFile),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
diagnostics.push({
|
|
47
|
+
file: context.filePath,
|
|
48
|
+
line: line + 1,
|
|
49
|
+
column: character + 1,
|
|
50
|
+
ruleId: "EVL010",
|
|
51
|
+
severity: "warning",
|
|
52
|
+
message: `Data transformation .${methodName}() inside Composite factory — transform data before passing it as props`,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
ts.forEachChild(node, (child) => checkNode(child, context, diagnostics));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const evl010CompositeNoTransformRule: LintRule = {
|
|
61
|
+
id: "EVL010",
|
|
62
|
+
severity: "warning",
|
|
63
|
+
category: "style",
|
|
64
|
+
check(context: LintContext): LintDiagnostic[] {
|
|
65
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
66
|
+
checkNode(context.sourceFile, context, diagnostics);
|
|
67
|
+
return diagnostics;
|
|
68
|
+
},
|
|
69
|
+
};
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import * as ts from "typescript";
|
|
3
|
+
import { exportRequiredRule } from "./export-required";
|
|
4
|
+
import type { LintContext } from "../rule";
|
|
5
|
+
|
|
6
|
+
function createContext(code: string, filePath = "test.ts"): LintContext {
|
|
7
|
+
const sourceFile = ts.createSourceFile(
|
|
8
|
+
filePath,
|
|
9
|
+
code,
|
|
10
|
+
ts.ScriptTarget.Latest,
|
|
11
|
+
true
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
sourceFile,
|
|
16
|
+
entities: [],
|
|
17
|
+
filePath,
|
|
18
|
+
lexicon: undefined,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("COR008: export-required", () => {
|
|
23
|
+
test("rule metadata", () => {
|
|
24
|
+
expect(exportRequiredRule.id).toBe("COR008");
|
|
25
|
+
expect(exportRequiredRule.severity).toBe("warning");
|
|
26
|
+
expect(exportRequiredRule.category).toBe("correctness");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("does not trigger when declarable is assigned to exported variable", () => {
|
|
30
|
+
const code = `
|
|
31
|
+
class Bucket implements Declarable {
|
|
32
|
+
readonly [Symbol.for("chant.declarable")] = true;
|
|
33
|
+
readonly entityType = "Bucket";
|
|
34
|
+
}
|
|
35
|
+
export const bucket = new Bucket();
|
|
36
|
+
`;
|
|
37
|
+
const context = createContext(code);
|
|
38
|
+
const diagnostics = exportRequiredRule.check(context);
|
|
39
|
+
expect(diagnostics).toHaveLength(0);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("triggers when declarable is created without export", () => {
|
|
43
|
+
const code = `
|
|
44
|
+
class Bucket implements Declarable {
|
|
45
|
+
readonly [Symbol.for("chant.declarable")] = true;
|
|
46
|
+
readonly entityType = "Bucket";
|
|
47
|
+
}
|
|
48
|
+
new Bucket();
|
|
49
|
+
`;
|
|
50
|
+
const context = createContext(code);
|
|
51
|
+
const diagnostics = exportRequiredRule.check(context);
|
|
52
|
+
|
|
53
|
+
expect(diagnostics).toHaveLength(1);
|
|
54
|
+
expect(diagnostics[0].ruleId).toBe("COR008");
|
|
55
|
+
expect(diagnostics[0].severity).toBe("warning");
|
|
56
|
+
expect(diagnostics[0].message).toContain("Bucket");
|
|
57
|
+
expect(diagnostics[0].message).toContain("is not exported");
|
|
58
|
+
expect(diagnostics[0].message).toContain("chant can discover it during synthesis");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("triggers when declarable is assigned to non-exported variable", () => {
|
|
62
|
+
const code = `
|
|
63
|
+
class Bucket implements Declarable {
|
|
64
|
+
readonly [Symbol.for("chant.declarable")] = true;
|
|
65
|
+
readonly entityType = "Bucket";
|
|
66
|
+
}
|
|
67
|
+
const bucket = new Bucket();
|
|
68
|
+
`;
|
|
69
|
+
const context = createContext(code);
|
|
70
|
+
const diagnostics = exportRequiredRule.check(context);
|
|
71
|
+
|
|
72
|
+
expect(diagnostics).toHaveLength(1);
|
|
73
|
+
expect(diagnostics[0].message).toContain("'bucket' (Bucket)");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("does not trigger on non-declarable class instantiations", () => {
|
|
77
|
+
const code = `
|
|
78
|
+
class RegularClass {
|
|
79
|
+
value: number;
|
|
80
|
+
constructor(val: number) {
|
|
81
|
+
this.value = val;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
new RegularClass(42);
|
|
85
|
+
`;
|
|
86
|
+
const context = createContext(code);
|
|
87
|
+
const diagnostics = exportRequiredRule.check(context);
|
|
88
|
+
expect(diagnostics).toHaveLength(0);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("does not trigger on exported non-declarable class", () => {
|
|
92
|
+
const code = `
|
|
93
|
+
class RegularClass {}
|
|
94
|
+
export const obj = new RegularClass();
|
|
95
|
+
`;
|
|
96
|
+
const context = createContext(code);
|
|
97
|
+
const diagnostics = exportRequiredRule.check(context);
|
|
98
|
+
expect(diagnostics).toHaveLength(0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("handles multiple declarable instances", () => {
|
|
102
|
+
const code = `
|
|
103
|
+
class Bucket implements Declarable {
|
|
104
|
+
readonly [Symbol.for("chant.declarable")] = true;
|
|
105
|
+
readonly entityType = "Bucket";
|
|
106
|
+
}
|
|
107
|
+
new Bucket();
|
|
108
|
+
new Bucket();
|
|
109
|
+
`;
|
|
110
|
+
const context = createContext(code);
|
|
111
|
+
const diagnostics = exportRequiredRule.check(context);
|
|
112
|
+
|
|
113
|
+
expect(diagnostics).toHaveLength(2);
|
|
114
|
+
expect(diagnostics[0].ruleId).toBe("COR008");
|
|
115
|
+
expect(diagnostics[1].ruleId).toBe("COR008");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("handles mixed exported and non-exported declarables", () => {
|
|
119
|
+
const code = `
|
|
120
|
+
class Bucket implements Declarable {
|
|
121
|
+
readonly [Symbol.for("chant.declarable")] = true;
|
|
122
|
+
readonly entityType = "Bucket";
|
|
123
|
+
}
|
|
124
|
+
export const bucket1 = new Bucket();
|
|
125
|
+
new Bucket();
|
|
126
|
+
export const bucket2 = new Bucket();
|
|
127
|
+
`;
|
|
128
|
+
const context = createContext(code);
|
|
129
|
+
const diagnostics = exportRequiredRule.check(context);
|
|
130
|
+
|
|
131
|
+
expect(diagnostics).toHaveLength(1);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("reports correct line and column numbers", () => {
|
|
135
|
+
const code = `
|
|
136
|
+
class Bucket implements Declarable {
|
|
137
|
+
readonly [Symbol.for("chant.declarable")] = true;
|
|
138
|
+
readonly entityType = "Bucket";
|
|
139
|
+
}
|
|
140
|
+
new Bucket();
|
|
141
|
+
`;
|
|
142
|
+
const context = createContext(code);
|
|
143
|
+
const diagnostics = exportRequiredRule.check(context);
|
|
144
|
+
|
|
145
|
+
expect(diagnostics).toHaveLength(1);
|
|
146
|
+
expect(diagnostics[0].line).toBe(6);
|
|
147
|
+
expect(diagnostics[0].column).toBeGreaterThan(0);
|
|
148
|
+
expect(diagnostics[0].file).toBe("test.ts");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("handles declarable with constructor arguments", () => {
|
|
152
|
+
const code = `
|
|
153
|
+
class Parameter implements Declarable {
|
|
154
|
+
readonly [Symbol.for("chant.declarable")] = true;
|
|
155
|
+
readonly entityType = "Parameter";
|
|
156
|
+
constructor(name: string, value: string) {}
|
|
157
|
+
}
|
|
158
|
+
new Parameter("key", "value");
|
|
159
|
+
`;
|
|
160
|
+
const context = createContext(code);
|
|
161
|
+
const diagnostics = exportRequiredRule.check(context);
|
|
162
|
+
|
|
163
|
+
expect(diagnostics).toHaveLength(1);
|
|
164
|
+
expect(diagnostics[0].message).toContain("'Parameter'");
|
|
165
|
+
expect(diagnostics[0].message).toContain("is not exported");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("allows exported variable with declarable", () => {
|
|
169
|
+
const code = `
|
|
170
|
+
class Bucket implements Declarable {
|
|
171
|
+
readonly [Symbol.for("chant.declarable")] = true;
|
|
172
|
+
readonly entityType = "Bucket";
|
|
173
|
+
}
|
|
174
|
+
export const myBucket = new Bucket();
|
|
175
|
+
`;
|
|
176
|
+
const context = createContext(code);
|
|
177
|
+
const diagnostics = exportRequiredRule.check(context);
|
|
178
|
+
expect(diagnostics).toHaveLength(0);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("handles nested new expressions", () => {
|
|
182
|
+
const code = `
|
|
183
|
+
class Bucket implements Declarable {
|
|
184
|
+
readonly [Symbol.for("chant.declarable")] = true;
|
|
185
|
+
readonly entityType = "Bucket";
|
|
186
|
+
}
|
|
187
|
+
class Wrapper {
|
|
188
|
+
constructor(bucket: Bucket) {}
|
|
189
|
+
}
|
|
190
|
+
new Wrapper(new Bucket());
|
|
191
|
+
`;
|
|
192
|
+
const context = createContext(code);
|
|
193
|
+
const diagnostics = exportRequiredRule.check(context);
|
|
194
|
+
|
|
195
|
+
// Inner new Bucket() should trigger
|
|
196
|
+
expect(diagnostics.length).toBeGreaterThanOrEqual(1);
|
|
197
|
+
const bucketDiagnostic = diagnostics.find(d => d.message.includes("Bucket"));
|
|
198
|
+
expect(bucketDiagnostic).toBeDefined();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("handles export statement with multiple declarations", () => {
|
|
202
|
+
const code = `
|
|
203
|
+
class Bucket implements Declarable {
|
|
204
|
+
readonly [Symbol.for("chant.declarable")] = true;
|
|
205
|
+
readonly entityType = "Bucket";
|
|
206
|
+
}
|
|
207
|
+
export const bucket1 = new Bucket(), bucket2 = new Bucket();
|
|
208
|
+
`;
|
|
209
|
+
const context = createContext(code);
|
|
210
|
+
const diagnostics = exportRequiredRule.check(context);
|
|
211
|
+
expect(diagnostics).toHaveLength(0);
|
|
212
|
+
});
|
|
213
|
+
});
|