@intentius/chant 0.0.8 → 0.0.10
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/package.json +2 -1
- package/src/bench.test.ts +1 -1
- package/src/cli/commands/doctor.ts +8 -3
- package/src/cli/commands/init.test.ts +44 -4
- package/src/cli/commands/init.ts +55 -23
- package/src/cli/commands/lint.ts +27 -13
- package/src/cli/handlers/init.ts +1 -0
- package/src/cli/lsp/server.ts +1 -1
- package/src/cli/main.ts +4 -0
- package/src/cli/mcp/server.test.ts +28 -2
- package/src/cli/mcp/tools/scaffold.ts +21 -3
- package/src/cli/registry.ts +1 -0
- package/src/cli/reporters/stylish.test.ts +212 -1
- package/src/cli/reporters/stylish.ts +133 -36
- package/src/codegen/docs-rules.test.ts +112 -0
- package/src/codegen/docs-rules.ts +129 -0
- package/src/codegen/docs.ts +3 -1
- package/src/codegen/generate-typescript.test.ts +64 -0
- package/src/codegen/generate-typescript.ts +13 -3
- package/src/codegen/package.ts +1 -1
- package/src/composite.test.ts +83 -16
- package/src/composite.ts +7 -5
- package/src/detectLexicon.test.ts +2 -2
- package/src/discovery/collect.test.ts +2 -2
- package/src/discovery/collect.ts +1 -1
- package/src/index.ts +1 -0
- package/src/lexicon-schema.ts +8 -0
- package/src/lexicon.ts +13 -1
- package/src/lint/declarative.ts +6 -0
- package/src/lint/engine.test.ts +287 -11
- package/src/lint/engine.ts +101 -23
- package/src/lint/rule-registry.test.ts +112 -0
- package/src/lint/rule-registry.ts +118 -0
- package/src/lint/rule.ts +8 -0
- package/src/lint/rules/cor017-composite-name-match.ts +2 -1
- package/src/lint/rules/cor018-composite-prefer-lexicon-type.ts +4 -3
- package/src/lint/rules/declarable-naming-convention.ts +1 -0
- package/src/lint/rules/evl001-non-literal-expression.ts +1 -0
- package/src/lint/rules/evl002-control-flow-resource.ts +1 -0
- package/src/lint/rules/evl003-dynamic-property-access.ts +1 -0
- package/src/lint/rules/evl004-spread-non-const.ts +1 -0
- package/src/lint/rules/evl005-resource-block-body.ts +1 -0
- package/src/lint/rules/evl007-invalid-siblings.ts +1 -0
- package/src/lint/rules/evl009-composite-no-constant.ts +1 -0
- package/src/lint/rules/evl010-composite-no-transform.ts +1 -0
- package/src/lint/rules/export-required.ts +1 -0
- package/src/lint/rules/file-declarable-limit.ts +1 -0
- package/src/lint/rules/flat-declarations.test.ts +8 -7
- package/src/lint/rules/flat-declarations.ts +2 -3
- package/src/lint/rules/no-cyclic-declarable-ref.ts +1 -0
- package/src/lint/rules/no-redundant-type-import.ts +1 -0
- package/src/lint/rules/no-redundant-value-cast.ts +1 -0
- package/src/lint/rules/no-string-ref.ts +1 -0
- package/src/lint/rules/no-unused-declarable-import.ts +1 -0
- package/src/lint/rules/no-unused-declarable.test.ts +8 -0
- package/src/lint/rules/no-unused-declarable.ts +4 -0
- package/src/lint/rules/single-concern-file.ts +1 -0
- package/src/lsp/lexicon-providers.ts +7 -0
- package/src/lsp/types.ts +1 -0
- package/src/resource-attributes.test.ts +79 -0
- package/src/resource-attributes.ts +42 -0
- package/src/runtime.ts +4 -3
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { buildRuleRegistry } from "./rule-registry";
|
|
3
|
+
import type { LintRule, LintContext, LintDiagnostic } from "./rule";
|
|
4
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "./post-synth";
|
|
5
|
+
|
|
6
|
+
function mockRule(id: string, overrides: Partial<LintRule> = {}): LintRule {
|
|
7
|
+
return {
|
|
8
|
+
id,
|
|
9
|
+
severity: "warning",
|
|
10
|
+
category: "correctness",
|
|
11
|
+
description: `Description for ${id}`,
|
|
12
|
+
check: (): LintDiagnostic[] => [],
|
|
13
|
+
...overrides,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function mockCheck(id: string, description: string): PostSynthCheck {
|
|
18
|
+
return {
|
|
19
|
+
id,
|
|
20
|
+
description,
|
|
21
|
+
check: (): PostSynthDiagnostic[] => [],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("buildRuleRegistry", () => {
|
|
26
|
+
test("collects core rules", () => {
|
|
27
|
+
const entries = buildRuleRegistry([
|
|
28
|
+
mockRule("COR001"),
|
|
29
|
+
mockRule("COR008"),
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
expect(entries).toHaveLength(2);
|
|
33
|
+
expect(entries[0].id).toBe("COR001");
|
|
34
|
+
expect(entries[0].source).toBe("core");
|
|
35
|
+
expect(entries[0].phase).toBe("pre-synth");
|
|
36
|
+
expect(entries[0].description).toBe("Description for COR001");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("collects plugin lint rules", () => {
|
|
40
|
+
const entries = buildRuleRegistry([], [
|
|
41
|
+
{
|
|
42
|
+
name: "aws",
|
|
43
|
+
rules: [mockRule("WAW001", { category: "security" })],
|
|
44
|
+
},
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
expect(entries).toHaveLength(1);
|
|
48
|
+
expect(entries[0].source).toBe("aws");
|
|
49
|
+
expect(entries[0].category).toBe("security");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("collects plugin post-synth checks", () => {
|
|
53
|
+
const entries = buildRuleRegistry([], [
|
|
54
|
+
{
|
|
55
|
+
name: "aws",
|
|
56
|
+
postSynthChecks: [
|
|
57
|
+
mockCheck("WAW018", "S3 public access not blocked"),
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
expect(entries).toHaveLength(1);
|
|
63
|
+
expect(entries[0].phase).toBe("post-synth");
|
|
64
|
+
expect(entries[0].hasAutoFix).toBe(false);
|
|
65
|
+
expect(entries[0].category).toBe("security");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("sorts entries by ID", () => {
|
|
69
|
+
const entries = buildRuleRegistry([
|
|
70
|
+
mockRule("COR008"),
|
|
71
|
+
mockRule("COR001"),
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
expect(entries[0].id).toBe("COR001");
|
|
75
|
+
expect(entries[1].id).toBe("COR008");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("detects auto-fix capability", () => {
|
|
79
|
+
const withFix = mockRule("COR001");
|
|
80
|
+
withFix.fix = () => [];
|
|
81
|
+
const withoutFix = mockRule("COR002");
|
|
82
|
+
|
|
83
|
+
const entries = buildRuleRegistry([withFix, withoutFix]);
|
|
84
|
+
|
|
85
|
+
expect(entries.find((e) => e.id === "COR001")?.hasAutoFix).toBe(true);
|
|
86
|
+
expect(entries.find((e) => e.id === "COR002")?.hasAutoFix).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("falls back to rule ID when description is missing", () => {
|
|
90
|
+
const rule = mockRule("COR001");
|
|
91
|
+
delete (rule as Record<string, unknown>).description;
|
|
92
|
+
|
|
93
|
+
const entries = buildRuleRegistry([rule]);
|
|
94
|
+
expect(entries[0].description).toBe("COR001");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("combines core and plugin rules", () => {
|
|
98
|
+
const entries = buildRuleRegistry(
|
|
99
|
+
[mockRule("COR001")],
|
|
100
|
+
[
|
|
101
|
+
{
|
|
102
|
+
name: "aws",
|
|
103
|
+
rules: [mockRule("WAW001")],
|
|
104
|
+
postSynthChecks: [mockCheck("WAW010", "Redundant DependsOn")],
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
expect(entries).toHaveLength(3);
|
|
110
|
+
expect(entries.map((e) => e.id)).toEqual(["COR001", "WAW001", "WAW010"]);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule registry — collects metadata from all rule sources into a flat, sortable array.
|
|
3
|
+
* Used for documentation generation and programmatic rule introspection.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { LintRule, Severity, Category } from "./rule";
|
|
7
|
+
import type { PostSynthCheck } from "./post-synth";
|
|
8
|
+
|
|
9
|
+
export interface RuleEntry {
|
|
10
|
+
/** Unique rule ID (e.g. "COR001", "WAW018") */
|
|
11
|
+
id: string;
|
|
12
|
+
/** Human-readable description of what this rule checks */
|
|
13
|
+
description: string;
|
|
14
|
+
/** Rule category */
|
|
15
|
+
category: Category;
|
|
16
|
+
/** Default severity level */
|
|
17
|
+
defaultSeverity: Severity;
|
|
18
|
+
/** Source of the rule ("core" or lexicon name) */
|
|
19
|
+
source: "core" | string;
|
|
20
|
+
/** Phase the rule runs in */
|
|
21
|
+
phase: "pre-synth" | "post-synth";
|
|
22
|
+
/** Whether this rule provides automatic fixes */
|
|
23
|
+
hasAutoFix: boolean;
|
|
24
|
+
/** Link to rule documentation */
|
|
25
|
+
helpUri?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build a rule registry from core rules and plugin-provided rules/checks.
|
|
30
|
+
*
|
|
31
|
+
* @param coreRules - Core LintRules (COR/EVL rules from packages/core)
|
|
32
|
+
* @param plugins - Array of plugin contributions with name, rules, and post-synth checks
|
|
33
|
+
* @returns Flat array of RuleEntry sorted by ID
|
|
34
|
+
*/
|
|
35
|
+
export function buildRuleRegistry(
|
|
36
|
+
coreRules: LintRule[],
|
|
37
|
+
plugins: Array<{
|
|
38
|
+
name: string;
|
|
39
|
+
rules?: LintRule[];
|
|
40
|
+
postSynthChecks?: PostSynthCheck[];
|
|
41
|
+
}> = [],
|
|
42
|
+
): RuleEntry[] {
|
|
43
|
+
const entries: RuleEntry[] = [];
|
|
44
|
+
|
|
45
|
+
// Core lint rules
|
|
46
|
+
for (const rule of coreRules) {
|
|
47
|
+
entries.push({
|
|
48
|
+
id: rule.id,
|
|
49
|
+
description: rule.description ?? rule.id,
|
|
50
|
+
category: rule.category,
|
|
51
|
+
defaultSeverity: rule.severity,
|
|
52
|
+
source: "core",
|
|
53
|
+
phase: "pre-synth",
|
|
54
|
+
hasAutoFix: typeof rule.fix === "function",
|
|
55
|
+
helpUri: rule.helpUri,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Plugin-provided lint rules and post-synth checks
|
|
60
|
+
for (const plugin of plugins) {
|
|
61
|
+
if (plugin.rules) {
|
|
62
|
+
for (const rule of plugin.rules) {
|
|
63
|
+
entries.push({
|
|
64
|
+
id: rule.id,
|
|
65
|
+
description: rule.description ?? rule.id,
|
|
66
|
+
category: rule.category,
|
|
67
|
+
defaultSeverity: rule.severity,
|
|
68
|
+
source: plugin.name,
|
|
69
|
+
phase: "pre-synth",
|
|
70
|
+
hasAutoFix: typeof rule.fix === "function",
|
|
71
|
+
helpUri: rule.helpUri,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (plugin.postSynthChecks) {
|
|
77
|
+
for (const check of plugin.postSynthChecks) {
|
|
78
|
+
entries.push({
|
|
79
|
+
id: check.id,
|
|
80
|
+
description: check.description,
|
|
81
|
+
category: inferPostSynthCategory(check.id),
|
|
82
|
+
defaultSeverity: inferPostSynthSeverity(check.id, check.description),
|
|
83
|
+
source: plugin.name,
|
|
84
|
+
phase: "post-synth",
|
|
85
|
+
hasAutoFix: false,
|
|
86
|
+
helpUri: `https://chant.dev/lint-rules/${check.id.toLowerCase()}`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return entries.sort((a, b) => a.id.localeCompare(b.id));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Infer category for post-synth checks from rule ID prefix.
|
|
97
|
+
* WAW018+ are security rules, existing WAW/COR/EXT default to correctness.
|
|
98
|
+
*/
|
|
99
|
+
function inferPostSynthCategory(id: string): Category {
|
|
100
|
+
const num = parseInt(id.replace(/\D/g, ""), 10);
|
|
101
|
+
if (id.startsWith("WAW") && num >= 18) return "security";
|
|
102
|
+
return "correctness";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Infer default severity for post-synth checks.
|
|
107
|
+
* Security checks with "error" level: WAW018, WAW019, WAW021.
|
|
108
|
+
*/
|
|
109
|
+
function inferPostSynthSeverity(id: string, description: string): Severity {
|
|
110
|
+
// Check if the description hints at severity
|
|
111
|
+
if (description.toLowerCase().includes("error")) return "error";
|
|
112
|
+
// Known error-level security checks
|
|
113
|
+
const errorIds = new Set(["WAW018", "WAW019", "WAW021"]);
|
|
114
|
+
if (errorIds.has(id)) return "error";
|
|
115
|
+
// Info-level checks
|
|
116
|
+
if (id === "WAW027") return "info";
|
|
117
|
+
return "warning";
|
|
118
|
+
}
|
package/src/lint/rule.ts
CHANGED
|
@@ -34,6 +34,10 @@ export interface LintDiagnostic {
|
|
|
34
34
|
line: number;
|
|
35
35
|
/** Column number (1-based) */
|
|
36
36
|
column: number;
|
|
37
|
+
/** End line number (1-based), when the diagnostic spans a range */
|
|
38
|
+
endLine?: number;
|
|
39
|
+
/** End column number (1-based), when the diagnostic spans a range */
|
|
40
|
+
endColumn?: number;
|
|
37
41
|
/** ID of the rule that produced this diagnostic */
|
|
38
42
|
ruleId: string;
|
|
39
43
|
/** Severity level */
|
|
@@ -73,6 +77,10 @@ export interface LintRule {
|
|
|
73
77
|
severity: Severity;
|
|
74
78
|
/** Category for grouping */
|
|
75
79
|
category: Category;
|
|
80
|
+
/** Human-readable description of what this rule checks */
|
|
81
|
+
description?: string;
|
|
82
|
+
/** Link to rule documentation */
|
|
83
|
+
helpUri?: string;
|
|
76
84
|
/** Check the code and return diagnostics */
|
|
77
85
|
check(context: LintContext, options?: Record<string, unknown>): LintDiagnostic[];
|
|
78
86
|
/** Optionally provide fixes for issues found */
|
|
@@ -7,7 +7,7 @@ import { isCompositeCallee } from "./composite-scope";
|
|
|
7
7
|
*
|
|
8
8
|
* The second argument to Composite() (the name string) must match the
|
|
9
9
|
* const variable it's assigned to. This name is used in resource expansion
|
|
10
|
-
* (e.g.
|
|
10
|
+
* (e.g. healthApiRole, healthApiFunc) and in error messages.
|
|
11
11
|
*
|
|
12
12
|
* Triggers on: const LambdaApi = Composite(fn, "MyFunction") — mismatch
|
|
13
13
|
* Triggers on: const LambdaApi = Composite(fn) — missing name
|
|
@@ -100,6 +100,7 @@ export const cor017CompositeNameMatchRule: LintRule = {
|
|
|
100
100
|
id: "COR017",
|
|
101
101
|
severity: "error",
|
|
102
102
|
category: "correctness",
|
|
103
|
+
description: "Composite name argument must match the variable it is assigned to",
|
|
103
104
|
check(context: LintContext): LintDiagnostic[] {
|
|
104
105
|
const diagnostics: LintDiagnostic[] = [];
|
|
105
106
|
checkNode(context.sourceFile, context, diagnostics);
|
|
@@ -5,7 +5,7 @@ import { isCompositeCallee } from "./composite-scope";
|
|
|
5
5
|
/**
|
|
6
6
|
* COR018: Prefer lexicon property types in Composite props
|
|
7
7
|
*
|
|
8
|
-
* Composite prop interfaces should use lexicon property types (via the
|
|
8
|
+
* Composite prop interfaces should use lexicon property types (via the lexicon package)
|
|
9
9
|
* instead of locally-declared interfaces or type aliases. Local types next to
|
|
10
10
|
* a Composite definition often duplicate existing lexicon property types.
|
|
11
11
|
*
|
|
@@ -13,7 +13,7 @@ import { isCompositeCallee } from "./composite-scope";
|
|
|
13
13
|
* in the same file as the Composite (excluding the props interface itself)
|
|
14
14
|
* OK: InstanceType<typeof _.Role_Policy>
|
|
15
15
|
* OK: primitives (string, number, boolean)
|
|
16
|
-
* OK:
|
|
16
|
+
* OK: lexicon-imported types
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
/**
|
|
@@ -60,7 +60,7 @@ function findLocalTypeDeclarations(sourceFile: ts.SourceFile): Map<string, ts.No
|
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
62
|
* Collect type references used in a props interface's field types.
|
|
63
|
-
* Returns names of locally-referenced types (not primitives, not from
|
|
63
|
+
* Returns names of locally-referenced types (not primitives, not from the lexicon package).
|
|
64
64
|
*/
|
|
65
65
|
function collectFieldTypeRefs(
|
|
66
66
|
propsDecl: ts.Node,
|
|
@@ -161,6 +161,7 @@ export const cor018CompositePreferLexiconTypeRule: LintRule = {
|
|
|
161
161
|
id: "COR018",
|
|
162
162
|
severity: "info",
|
|
163
163
|
category: "style",
|
|
164
|
+
description: "Prefer lexicon property types over local interfaces in Composite props",
|
|
164
165
|
check(context: LintContext): LintDiagnostic[] {
|
|
165
166
|
return checkFile(context);
|
|
166
167
|
},
|
|
@@ -62,6 +62,7 @@ export const declarableNamingConventionRule: LintRule = {
|
|
|
62
62
|
id: "COR005",
|
|
63
63
|
severity: "warning",
|
|
64
64
|
category: "style",
|
|
65
|
+
description: "Exported declarable instances must use camelCase naming",
|
|
65
66
|
check(context: LintContext): LintDiagnostic[] {
|
|
66
67
|
const diagnostics: LintDiagnostic[] = [];
|
|
67
68
|
checkNode(context.sourceFile, context, diagnostics);
|
|
@@ -141,6 +141,7 @@ export const evl001NonLiteralExpressionRule: LintRule = {
|
|
|
141
141
|
id: "EVL001",
|
|
142
142
|
severity: "error",
|
|
143
143
|
category: "correctness",
|
|
144
|
+
description: "Resource constructor property values must be statically evaluable — no function calls",
|
|
144
145
|
check(context: LintContext): LintDiagnostic[] {
|
|
145
146
|
const diagnostics: LintDiagnostic[] = [];
|
|
146
147
|
checkNode(context.sourceFile, context, diagnostics);
|
|
@@ -53,6 +53,7 @@ export const evl002ControlFlowResourceRule: LintRule = {
|
|
|
53
53
|
id: "EVL002",
|
|
54
54
|
severity: "error",
|
|
55
55
|
category: "correctness",
|
|
56
|
+
description: "Resource constructors must not appear inside control flow statements",
|
|
56
57
|
check(context: LintContext): LintDiagnostic[] {
|
|
57
58
|
const diagnostics: LintDiagnostic[] = [];
|
|
58
59
|
checkNode(context.sourceFile, context, diagnostics);
|
|
@@ -33,6 +33,7 @@ export const evl003DynamicPropertyAccessRule: LintRule = {
|
|
|
33
33
|
id: "EVL003",
|
|
34
34
|
severity: "error",
|
|
35
35
|
category: "correctness",
|
|
36
|
+
description: "Computed property access must use a string or numeric literal key",
|
|
36
37
|
check(context: LintContext): LintDiagnostic[] {
|
|
37
38
|
const diagnostics: LintDiagnostic[] = [];
|
|
38
39
|
checkNode(context.sourceFile, context, diagnostics);
|
|
@@ -103,6 +103,7 @@ export const evl004SpreadNonConstRule: LintRule = {
|
|
|
103
103
|
id: "EVL004",
|
|
104
104
|
severity: "error",
|
|
105
105
|
category: "correctness",
|
|
106
|
+
description: "Spread expressions must reference a const variable or literal, not a dynamic source",
|
|
106
107
|
check(context: LintContext): LintDiagnostic[] {
|
|
107
108
|
const diagnostics: LintDiagnostic[] = [];
|
|
108
109
|
checkNode(context.sourceFile, context, diagnostics);
|
|
@@ -41,6 +41,7 @@ export const evl005ResourceBlockBodyRule: LintRule = {
|
|
|
41
41
|
id: "EVL005",
|
|
42
42
|
severity: "error",
|
|
43
43
|
category: "correctness",
|
|
44
|
+
description: "The resource() callback must use expression body, not block body",
|
|
44
45
|
check(context: LintContext): LintDiagnostic[] {
|
|
45
46
|
const diagnostics: LintDiagnostic[] = [];
|
|
46
47
|
checkNode(context.sourceFile, context, diagnostics);
|
|
@@ -131,6 +131,7 @@ export const evl007InvalidSiblingsRule: LintRule = {
|
|
|
131
131
|
id: "EVL007",
|
|
132
132
|
severity: "error",
|
|
133
133
|
category: "correctness",
|
|
134
|
+
description: "Validates that siblings access in Composite factories references existing member keys",
|
|
134
135
|
check(context: LintContext): LintDiagnostic[] {
|
|
135
136
|
const diagnostics: LintDiagnostic[] = [];
|
|
136
137
|
checkNode(context.sourceFile, context, diagnostics);
|
|
@@ -183,6 +183,7 @@ export const evl009CompositeNoConstantRule: LintRule = {
|
|
|
183
183
|
id: "EVL009",
|
|
184
184
|
severity: "warning",
|
|
185
185
|
category: "style",
|
|
186
|
+
description: "Flags extractable constants inside Composite factories — move to a separate file",
|
|
186
187
|
check(context: LintContext): LintDiagnostic[] {
|
|
187
188
|
const diagnostics: LintDiagnostic[] = [];
|
|
188
189
|
const importedNames = collectImportedNames(context.sourceFile);
|
|
@@ -61,6 +61,7 @@ export const evl010CompositeNoTransformRule: LintRule = {
|
|
|
61
61
|
id: "EVL010",
|
|
62
62
|
severity: "warning",
|
|
63
63
|
category: "style",
|
|
64
|
+
description: "Flags data transformations (map, filter, reduce) inside Composite factories",
|
|
64
65
|
check(context: LintContext): LintDiagnostic[] {
|
|
65
66
|
const diagnostics: LintDiagnostic[] = [];
|
|
66
67
|
checkNode(context.sourceFile, context, diagnostics);
|
|
@@ -149,6 +149,7 @@ export const exportRequiredRule: LintRule = {
|
|
|
149
149
|
id: "COR008",
|
|
150
150
|
severity: "warning",
|
|
151
151
|
category: "correctness",
|
|
152
|
+
description: "Detects declarable instances that are not exported for chant discovery",
|
|
152
153
|
check(context: LintContext): LintDiagnostic[] {
|
|
153
154
|
const diagnostics: LintDiagnostic[] = [];
|
|
154
155
|
const exportInfo = collectExportInfo(context.sourceFile);
|
|
@@ -73,6 +73,7 @@ export const fileDeclarableLimitRule: LintRule = {
|
|
|
73
73
|
id: "COR009",
|
|
74
74
|
severity: "warning",
|
|
75
75
|
category: "style",
|
|
76
|
+
description: "Limits the number of Declarable instances per file to encourage splitting by concern",
|
|
76
77
|
check(context: LintContext, options?: Record<string, unknown>): LintDiagnostic[] {
|
|
77
78
|
const limit = (typeof options?.max === "number" ? options.max : null) ?? DECLARABLE_LIMIT;
|
|
78
79
|
const instances: ts.NewExpression[] = [];
|
|
@@ -163,7 +163,7 @@ describe("COR001: flat-declarations", () => {
|
|
|
163
163
|
expect(diagnostics[0].ruleId).toBe("COR001");
|
|
164
164
|
});
|
|
165
165
|
|
|
166
|
-
test("
|
|
166
|
+
test("triggers inside Composite() factory callback", () => {
|
|
167
167
|
const code = `
|
|
168
168
|
const MyComposite = Composite((props) => {
|
|
169
169
|
const role = new Role({
|
|
@@ -177,10 +177,11 @@ describe("COR001: flat-declarations", () => {
|
|
|
177
177
|
`;
|
|
178
178
|
const context = createContext(code);
|
|
179
179
|
const diagnostics = flatDeclarationsRule.check(context);
|
|
180
|
-
expect(diagnostics).toHaveLength(
|
|
180
|
+
expect(diagnostics).toHaveLength(1);
|
|
181
|
+
expect(diagnostics[0].ruleId).toBe("COR001");
|
|
181
182
|
});
|
|
182
183
|
|
|
183
|
-
test("
|
|
184
|
+
test("triggers inside _.Composite() factory callback", () => {
|
|
184
185
|
const code = `
|
|
185
186
|
const MyComposite = _.Composite((props) => {
|
|
186
187
|
const role = new _.Role({
|
|
@@ -191,10 +192,11 @@ describe("COR001: flat-declarations", () => {
|
|
|
191
192
|
`;
|
|
192
193
|
const context = createContext(code);
|
|
193
194
|
const diagnostics = flatDeclarationsRule.check(context);
|
|
194
|
-
expect(diagnostics).toHaveLength(
|
|
195
|
+
expect(diagnostics).toHaveLength(1);
|
|
196
|
+
expect(diagnostics[0].ruleId).toBe("COR001");
|
|
195
197
|
});
|
|
196
198
|
|
|
197
|
-
test("
|
|
199
|
+
test("triggers both inside and outside Composite() in same file", () => {
|
|
198
200
|
const code = `
|
|
199
201
|
const MyComposite = Composite((props) => {
|
|
200
202
|
const role = new Role({ config: { value: 1 } });
|
|
@@ -204,7 +206,6 @@ describe("COR001: flat-declarations", () => {
|
|
|
204
206
|
`;
|
|
205
207
|
const context = createContext(code);
|
|
206
208
|
const diagnostics = flatDeclarationsRule.check(context);
|
|
207
|
-
expect(diagnostics).toHaveLength(
|
|
208
|
-
expect(diagnostics[0].message).toContain("Inline object");
|
|
209
|
+
expect(diagnostics).toHaveLength(2);
|
|
209
210
|
});
|
|
210
211
|
});
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as ts from "typescript";
|
|
2
2
|
import type { LintRule, LintContext, LintDiagnostic } from "../rule";
|
|
3
|
-
import { isInsideCompositeFactory } from "./composite-scope";
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* COR001: No inline objects in Declarable constructors
|
|
@@ -17,8 +16,7 @@ import { isInsideCompositeFactory } from "./composite-scope";
|
|
|
17
16
|
|
|
18
17
|
function checkNode(node: ts.Node, context: LintContext, diagnostics: LintDiagnostic[]): void {
|
|
19
18
|
// Check for NewExpression nodes (constructor calls)
|
|
20
|
-
|
|
21
|
-
if (ts.isNewExpression(node) && !isInsideCompositeFactory(node)) {
|
|
19
|
+
if (ts.isNewExpression(node)) {
|
|
22
20
|
// Check if the first argument is an object literal
|
|
23
21
|
if (node.arguments && node.arguments.length > 0) {
|
|
24
22
|
const firstArg = node.arguments[0];
|
|
@@ -62,6 +60,7 @@ export const flatDeclarationsRule: LintRule = {
|
|
|
62
60
|
id: "COR001",
|
|
63
61
|
severity: "warning",
|
|
64
62
|
category: "style",
|
|
63
|
+
description: "Detects inline object literals in Declarable constructors — extract to named exports",
|
|
65
64
|
check(context: LintContext): LintDiagnostic[] {
|
|
66
65
|
const diagnostics: LintDiagnostic[] = [];
|
|
67
66
|
checkNode(context.sourceFile, context, diagnostics);
|
|
@@ -128,6 +128,7 @@ export const noCyclicDeclarableRefRule: LintRule = {
|
|
|
128
128
|
id: "COR011",
|
|
129
129
|
severity: "error",
|
|
130
130
|
category: "correctness",
|
|
131
|
+
description: "Detects circular references between declarables in the same file",
|
|
131
132
|
check(context: LintContext): LintDiagnostic[] {
|
|
132
133
|
const diagnostics: LintDiagnostic[] = [];
|
|
133
134
|
const declarables = collectExportedDeclarables(context.sourceFile);
|
|
@@ -13,6 +13,7 @@ export const noRedundantTypeImportRule: LintRule = {
|
|
|
13
13
|
id: "COR012",
|
|
14
14
|
severity: "warning",
|
|
15
15
|
category: "style",
|
|
16
|
+
description: "Flags redundant type imports when a namespace import already provides access",
|
|
16
17
|
check(context: LintContext): LintDiagnostic[] {
|
|
17
18
|
const diagnostics: LintDiagnostic[] = [];
|
|
18
19
|
const sf = context.sourceFile;
|
|
@@ -38,6 +38,7 @@ export const noRedundantValueCastRule: LintRule = {
|
|
|
38
38
|
id: "COR015",
|
|
39
39
|
severity: "warning",
|
|
40
40
|
category: "style",
|
|
41
|
+
description: "Flags redundant 'as Value<...>' casts — Intrinsic types already satisfy Value<T>",
|
|
41
42
|
check(context: LintContext): LintDiagnostic[] {
|
|
42
43
|
const diagnostics: LintDiagnostic[] = [];
|
|
43
44
|
checkNode(context.sourceFile, context, diagnostics);
|
|
@@ -15,6 +15,7 @@ export const noStringRefRule: LintRule = {
|
|
|
15
15
|
id: "COR003",
|
|
16
16
|
severity: "warning",
|
|
17
17
|
category: "correctness",
|
|
18
|
+
description: "Flags string-based GetAtt() and Ref() calls — use typed property access instead",
|
|
18
19
|
check(context: LintContext): LintDiagnostic[] {
|
|
19
20
|
const diagnostics: LintDiagnostic[] = [];
|
|
20
21
|
const sf = context.sourceFile;
|
|
@@ -77,6 +77,7 @@ export const noUnusedDeclarableImportRule: LintRule = {
|
|
|
77
77
|
id: "COR010",
|
|
78
78
|
severity: "warning",
|
|
79
79
|
category: "style",
|
|
80
|
+
description: "Flags unused namespace imports from @intentius/chant packages",
|
|
80
81
|
check(context: LintContext): LintDiagnostic[] {
|
|
81
82
|
const diagnostics: LintDiagnostic[] = [];
|
|
82
83
|
const imports = collectNamespaceImports(context.sourceFile);
|
|
@@ -124,6 +124,14 @@ describe("COR004: no-unused-declarable", () => {
|
|
|
124
124
|
expect(diags).toHaveLength(0);
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
+
test("OK for Parameter declarations (inherently cross-file)", () => {
|
|
128
|
+
const ctx = createContext(
|
|
129
|
+
`export const environment = new Parameter("String", { description: "env" });`,
|
|
130
|
+
);
|
|
131
|
+
const diags = noUnusedDeclarableRule.check(ctx);
|
|
132
|
+
expect(diags).toHaveLength(0);
|
|
133
|
+
});
|
|
134
|
+
|
|
127
135
|
test("detects reference in array literal", () => {
|
|
128
136
|
const ctx = createContext(
|
|
129
137
|
`export const bucket = new Bucket({});\n` + `export const list = [bucket];`,
|
|
@@ -51,6 +51,9 @@ function collectExportedDeclarables(sourceFile: ts.SourceFile): DeclarableInfo[]
|
|
|
51
51
|
const className = getNewExpressionClassName(decl.initializer);
|
|
52
52
|
if (!className || !isCapitalized(className)) continue;
|
|
53
53
|
|
|
54
|
+
// Parameters are inherently cross-file (declared in params.ts, consumed via Ref() elsewhere)
|
|
55
|
+
if (className === "Parameter") continue;
|
|
56
|
+
|
|
54
57
|
declarables.push({
|
|
55
58
|
name: decl.name.text,
|
|
56
59
|
node,
|
|
@@ -92,6 +95,7 @@ export const noUnusedDeclarableRule: LintRule = {
|
|
|
92
95
|
id: "COR004",
|
|
93
96
|
severity: "warning",
|
|
94
97
|
category: "correctness",
|
|
98
|
+
description: "Detects exported declarables that are never referenced in the same file",
|
|
95
99
|
check(context: LintContext): LintDiagnostic[] {
|
|
96
100
|
const diagnostics: LintDiagnostic[] = [];
|
|
97
101
|
const declarables = collectExportedDeclarables(context.sourceFile);
|
|
@@ -64,6 +64,7 @@ export const singleConcernFileRule: LintRule = {
|
|
|
64
64
|
id: "COR013",
|
|
65
65
|
severity: "info",
|
|
66
66
|
category: "style",
|
|
67
|
+
description: "Flags files mixing resource Declarables with configuration Declarables",
|
|
67
68
|
check(context: LintContext): LintDiagnostic[] {
|
|
68
69
|
const newExpressions: NewExpressionInfo[] = [];
|
|
69
70
|
collectNewExpressions(context.sourceFile, context.sourceFile, newExpressions);
|
|
@@ -19,6 +19,10 @@ export interface LexiconEntry {
|
|
|
19
19
|
createOnly?: string[];
|
|
20
20
|
writeOnly?: string[];
|
|
21
21
|
primaryIdentifier?: string[];
|
|
22
|
+
deprecatedProperties?: string[];
|
|
23
|
+
conditionalCreateOnly?: string[];
|
|
24
|
+
replacementStrategy?: "delete_then_create" | "create_then_delete";
|
|
25
|
+
tagging?: { taggable: boolean; tagOnCreate: boolean; tagUpdatable: boolean };
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
// ── LexiconIndex ───────────────────────────────────────────────────
|
|
@@ -109,8 +113,10 @@ export function lexiconCompletions(
|
|
|
109
113
|
|
|
110
114
|
if (constructorMatch) {
|
|
111
115
|
const className = constructorMatch[1];
|
|
116
|
+
const entry = index.getEntry(className);
|
|
112
117
|
const props = index.getPropertyNames(className);
|
|
113
118
|
if (props.length > 0) {
|
|
119
|
+
const deprecatedSet = new Set(entry?.deprecatedProperties ?? []);
|
|
114
120
|
const filtered = wordAtCursor
|
|
115
121
|
? props.filter((p) => p.toLowerCase().startsWith(wordAtCursor.toLowerCase()))
|
|
116
122
|
: props;
|
|
@@ -119,6 +125,7 @@ export function lexiconCompletions(
|
|
|
119
125
|
insertText: p,
|
|
120
126
|
kind: "property" as const,
|
|
121
127
|
detail: `Property of ${className}`,
|
|
128
|
+
...(deprecatedSet.has(p) && { deprecated: true }),
|
|
122
129
|
}));
|
|
123
130
|
}
|
|
124
131
|
}
|