@intentius/chant 0.21.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build.d.ts +7 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/build-params-cli.d.ts +55 -0
- package/dist/cli/build-params-cli.d.ts.map +1 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts +14 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/lsp/server.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +33 -2
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/discover.d.ts +28 -0
- package/dist/components/discover.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +71 -9
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/index.d.ts +27 -4
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +39 -2
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/lint/engine.d.ts +11 -1
- package/dist/lint/engine.d.ts.map +1 -1
- package/dist/lint/rule.d.ts +14 -0
- package/dist/lint/rule.d.ts.map +1 -1
- package/dist/serializer-walker.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/build.ts +9 -0
- package/src/cli/build-params-cli.test.ts +139 -0
- package/src/cli/build-params-cli.ts +107 -0
- package/src/cli/commands/build.ts +25 -36
- package/src/cli/commands/check-lexicon-examples.ts +16 -1
- package/src/cli/commands/lint.test.ts +74 -0
- package/src/cli/commands/lint.ts +33 -9
- package/src/cli/handlers/build.test.ts +147 -0
- package/src/cli/handlers/build.ts +23 -8
- package/src/cli/handlers/run.test.ts +160 -5
- package/src/cli/handlers/run.ts +46 -8
- package/src/cli/lsp/server.ts +7 -2
- package/src/components/cli-support.test.ts +221 -3
- package/src/components/cli-support.ts +37 -6
- package/src/components/discover.test.ts +63 -1
- package/src/components/discover.ts +42 -0
- package/src/discovery/fold-import.test.ts +328 -1
- package/src/discovery/fold-import.ts +414 -31
- package/src/discovery/index.test.ts +131 -0
- package/src/discovery/index.ts +53 -8
- package/src/discovery/sandbox/fold-boundary.test.ts +254 -0
- package/src/fold/fold.ts +6 -2
- package/src/fold/subset.test.ts +95 -15
- package/src/fold/subset.ts +46 -5
- package/src/lint/engine.ts +12 -0
- package/src/lint/rule.ts +14 -0
- package/src/lint/rules/evl001-non-literal-expression.test.ts +39 -0
- package/src/lint/rules/evl001-non-literal-expression.ts +1 -1
- package/src/serializer-walker.ts +14 -0
package/src/fold/subset.ts
CHANGED
|
@@ -69,8 +69,22 @@ import { intrinsicCallFolds, type IntrinsicDef } from "../lexicon";
|
|
|
69
69
|
* the flow-sensitivity note below — the single divergence in the other
|
|
70
70
|
* direction, and the one this module treats as a wart). A caller with
|
|
71
71
|
* no registry degrades to "assume it runs", which is safe and cheap to
|
|
72
|
-
* reason about
|
|
73
|
-
*
|
|
72
|
+
* reason about.
|
|
73
|
+
*
|
|
74
|
+
* chant #1106 — EVL is no longer such a caller by default. `runLint`
|
|
75
|
+
* (../lint/engine.ts) takes the active lexicons' `IntrinsicDef[]` as a
|
|
76
|
+
* parameter and puts it on `LintContext.intrinsics`
|
|
77
|
+
* (../lint/rule.ts), and EVL001 (evl001-non-literal-expression.ts)
|
|
78
|
+
* passes it straight through to `checkObjectMember`. `chant lint`'s
|
|
79
|
+
* three CLI entry points (the `lint` command's initial pass, its
|
|
80
|
+
* `--fix` re-lint, and the LSP's per-file diagnostics) all resolve the
|
|
81
|
+
* project's lexicons and thread their intrinsics through, mirroring
|
|
82
|
+
* how `discover()` has done it for the fold path since #1039/#1105 —
|
|
83
|
+
* so `chant lint` on a real project no longer flags `Ref(...)` that
|
|
84
|
+
* `fold()` accepts. A `LintContext` built without that plumbing (a
|
|
85
|
+
* unit test constructing one directly, a consumer that hasn't
|
|
86
|
+
* resolved lexicons) still gets the pre-#1044 conservative answer —
|
|
87
|
+
* that path was never wrong, only stricter than it had to be.
|
|
74
88
|
* 3. Runtime *type* of a folded value — e.g. spreading `const n = 5`
|
|
75
89
|
* (`{...n}`) is shape-valid (`n` is a plain identifier) but `fold()`
|
|
76
90
|
* rejects it once it discovers `n` folds to a number, not an object.
|
|
@@ -141,6 +155,22 @@ export function isLiteralElementKey(node: ts.Expression): node is ts.StringLiter
|
|
|
141
155
|
return ts.isStringLiteral(node) || ts.isNumericLiteral(node);
|
|
142
156
|
}
|
|
143
157
|
|
|
158
|
+
/**
|
|
159
|
+
* A short, single-line rendering of `node`'s source text for embedding in a
|
|
160
|
+
* diagnostic message — never the raw `getText()`, which reproduces the
|
|
161
|
+
* node's ENTIRE source verbatim and can span dozens of lines for a real
|
|
162
|
+
* composite call or object literal (chant #1054: a fold fallback reason that
|
|
163
|
+
* embeds one of these buries the actual error after it, and breaks any
|
|
164
|
+
* line-oriented consumer of `[fold:run]` output). Internal whitespace
|
|
165
|
+
* (including newlines) collapses to a single space, and the result is capped
|
|
166
|
+
* to a bounded length so one pathological node can't blow out an otherwise
|
|
167
|
+
* one-line reason either.
|
|
168
|
+
*/
|
|
169
|
+
export function briefNodeText(node: ts.Node, maxLength = 60): string {
|
|
170
|
+
const collapsed = node.getText().replace(/\s+/g, " ").trim();
|
|
171
|
+
return collapsed.length > maxLength ? `${collapsed.slice(0, maxLength - 3)}...` : collapsed;
|
|
172
|
+
}
|
|
173
|
+
|
|
144
174
|
// ---------------------------------------------------------------------------
|
|
145
175
|
// Shared message builders — `fold()` and `findSubsetViolation` both call
|
|
146
176
|
// these so the diagnostic text for the same violation kind is the same
|
|
@@ -148,11 +178,11 @@ export function isLiteralElementKey(node: ts.Expression): node is ts.StringLiter
|
|
|
148
178
|
// ---------------------------------------------------------------------------
|
|
149
179
|
|
|
150
180
|
export function computedPropertyNameMessage(node: ts.PropertyName): string {
|
|
151
|
-
return `computed/dynamic property name not foldable: ${node
|
|
181
|
+
return `computed/dynamic property name not foldable: ${briefNodeText(node)}`;
|
|
152
182
|
}
|
|
153
183
|
|
|
154
184
|
export function dynamicElementAccessMessage(keyNode: ts.Expression): string {
|
|
155
|
-
return `dynamic property access — computed key must be a string or numeric literal: ${keyNode
|
|
185
|
+
return `dynamic property access — computed key must be a string or numeric literal: ${briefNodeText(keyNode)}`;
|
|
156
186
|
}
|
|
157
187
|
|
|
158
188
|
export const UNSUPPORTED_OBJECT_MEMBER_MESSAGE = "unsupported object member";
|
|
@@ -163,8 +193,19 @@ export function unsupportedBinaryMessage(opKind: ts.SyntaxKind): string {
|
|
|
163
193
|
return `unsupported binary operator: ${ts.SyntaxKind[opKind]}`;
|
|
164
194
|
}
|
|
165
195
|
|
|
196
|
+
/**
|
|
197
|
+
* chant #1054 — the ONE wording for "a bare function/method call used where
|
|
198
|
+
* chant needs a value it can fold." Before this, `fold()` (this message) and
|
|
199
|
+
* `../discovery/fold-import.ts`'s `resolveCallExpression` (a top-level
|
|
200
|
+
* export's own call-as-a-value check) had each grown their own hand-written
|
|
201
|
+
* copy — "function call as a value" here, "call expression as a value"
|
|
202
|
+
* there — for the identical rejection, which meant a tool grouping fallback
|
|
203
|
+
* reasons by text had to match both to avoid silently under-counting one of
|
|
204
|
+
* them. `resolveCallExpression` now calls this function directly instead of
|
|
205
|
+
* building its own string.
|
|
206
|
+
*/
|
|
166
207
|
export function callExpressionMessage(node: ts.CallExpression): string {
|
|
167
|
-
return `function call as a value is not foldable: ${node.expression
|
|
208
|
+
return `function call as a value is not foldable: ${briefNodeText(node.expression)}(...)`;
|
|
168
209
|
}
|
|
169
210
|
|
|
170
211
|
export function unsupportedExpressionMessage(node: ts.Node): string {
|
package/src/lint/engine.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LintRule, LintDiagnostic, LintContext } from "./rule";
|
|
2
|
+
import type { IntrinsicDef } from "../lexicon";
|
|
2
3
|
import { parseFile } from "./parser";
|
|
3
4
|
import { readFileSync } from "fs";
|
|
4
5
|
|
|
@@ -193,12 +194,22 @@ function isDiagnosticDisabled(
|
|
|
193
194
|
* @param files - Array of file paths to lint
|
|
194
195
|
* @param rules - Array of lint rules to execute
|
|
195
196
|
* @param ruleOptions - Optional map of rule ID to options object
|
|
197
|
+
* @param intrinsics - chant #1106 — the active lexicons' registered
|
|
198
|
+
* intrinsics (e.g. AWS's `Ref`, `GetAtt`), put on every file's
|
|
199
|
+
* `LintContext.intrinsics` so a rule built on `../fold/subset.ts`'s
|
|
200
|
+
* shared predicate (EVL001) answers exactly like `fold()` does for a
|
|
201
|
+
* registered, opted-in call, instead of degrading to "every call is a
|
|
202
|
+
* violation". Mirrors how `discover()` has threaded the same
|
|
203
|
+
* `IntrinsicDef[]` into the fold path since #1039/#1105. Optional and
|
|
204
|
+
* defaulting to none, so a caller that hasn't resolved a project's
|
|
205
|
+
* lexicons (a unit test, `bench.test.ts`) is unaffected.
|
|
196
206
|
* @returns LintRunResult with diagnostics and suppressed items
|
|
197
207
|
*/
|
|
198
208
|
export async function runLint(
|
|
199
209
|
files: string[],
|
|
200
210
|
rules: LintRule[],
|
|
201
211
|
ruleOptions?: Map<string, Record<string, unknown>>,
|
|
212
|
+
intrinsics?: readonly IntrinsicDef[],
|
|
202
213
|
): Promise<LintRunResult> {
|
|
203
214
|
const allDiagnostics: LintDiagnostic[] = [];
|
|
204
215
|
const allSuppressed: Array<LintDiagnostic & { reason?: string }> = [];
|
|
@@ -219,6 +230,7 @@ export async function runLint(
|
|
|
219
230
|
entities: [],
|
|
220
231
|
filePath,
|
|
221
232
|
lexicon: undefined,
|
|
233
|
+
intrinsics,
|
|
222
234
|
};
|
|
223
235
|
|
|
224
236
|
// Execute each rule
|
package/src/lint/rule.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type * as ts from "typescript";
|
|
2
|
+
import type { IntrinsicDef } from "../lexicon";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Severity level for lint diagnostics
|
|
@@ -60,6 +61,19 @@ export interface LintContext {
|
|
|
60
61
|
filePath: string;
|
|
61
62
|
/** Optional lexicon context (undefined for core rules) */
|
|
62
63
|
lexicon?: string;
|
|
64
|
+
/**
|
|
65
|
+
* chant #1106 — the active lexicons' registered intrinsics (`Ref`,
|
|
66
|
+
* `GetAtt`, ...), threaded down from `runLint` (../lint/engine.ts) so a
|
|
67
|
+
* rule built on the shared `../fold/subset.ts` predicate
|
|
68
|
+
* (`findSubsetViolation`/`checkObjectMember`, used by EVL001) gets the
|
|
69
|
+
* SAME answer `fold()` does for a registered, opted-in call. Mirrors how
|
|
70
|
+
* `discover()` has threaded `IntrinsicDef[]` into the fold path since
|
|
71
|
+
* #1039/#1105. Undefined when the caller hasn't resolved a project's
|
|
72
|
+
* lexicons (a bare unit test constructing a `LintContext` directly, for
|
|
73
|
+
* instance) — subset.ts then falls back to its pre-#1044 answer: every
|
|
74
|
+
* call is a violation.
|
|
75
|
+
*/
|
|
76
|
+
intrinsics?: readonly IntrinsicDef[];
|
|
63
77
|
}
|
|
64
78
|
|
|
65
79
|
/**
|
|
@@ -2,6 +2,7 @@ import { describe, test, expect } from "vitest";
|
|
|
2
2
|
import * as ts from "typescript";
|
|
3
3
|
import { evl001NonLiteralExpressionRule } from "./evl001-non-literal-expression";
|
|
4
4
|
import type { LintContext } from "../rule";
|
|
5
|
+
import type { IntrinsicDef } from "../../lexicon";
|
|
5
6
|
|
|
6
7
|
function createContext(code: string, filePath = "test.ts"): LintContext {
|
|
7
8
|
const sourceFile = ts.createSourceFile(filePath, code, ts.ScriptTarget.Latest, true);
|
|
@@ -155,4 +156,42 @@ describe("EVL001: non-literal-expression", () => {
|
|
|
155
156
|
expect(diags).toHaveLength(1);
|
|
156
157
|
expect(diags[0].ruleId).toBe("EVL001");
|
|
157
158
|
});
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* chant #1106 — `LintContext.intrinsics`, threaded from `runLint`, is what
|
|
162
|
+
* lets this rule answer a registered call-form intrinsic exactly like
|
|
163
|
+
* `fold()` does instead of flagging every call. See ../../fold/subset.ts
|
|
164
|
+
* and ../../fold/subset.test.ts for the shared predicate this rule calls.
|
|
165
|
+
*/
|
|
166
|
+
describe("context.intrinsics (#1106)", () => {
|
|
167
|
+
const REF: IntrinsicDef[] = [{ name: "Ref", isTag: false, foldsAsCall: true }];
|
|
168
|
+
|
|
169
|
+
test("flags a call to a registered, opted-in intrinsic when the context carries no registry", () => {
|
|
170
|
+
const ctx = createContext(`new Bucket({ name: Ref(env) });`);
|
|
171
|
+
const diags = evl001NonLiteralExpressionRule.check(ctx);
|
|
172
|
+
expect(diags).toHaveLength(1);
|
|
173
|
+
expect(diags[0].ruleId).toBe("EVL001");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("does not flag that same call once the context carries the registry", () => {
|
|
177
|
+
const ctx: LintContext = { ...createContext(`new Bucket({ name: Ref(env) });`), intrinsics: REF };
|
|
178
|
+
expect(evl001NonLiteralExpressionRule.check(ctx)).toHaveLength(0);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("a registered name WITHOUT the call opt-in still flags, even with the registry present", () => {
|
|
182
|
+
const notOptedIn: IntrinsicDef[] = [{ name: "Reference", isTag: false }];
|
|
183
|
+
const ctx: LintContext = {
|
|
184
|
+
...createContext(`new Bucket({ name: Reference(env) });`),
|
|
185
|
+
intrinsics: notOptedIn,
|
|
186
|
+
};
|
|
187
|
+
const diags = evl001NonLiteralExpressionRule.check(ctx);
|
|
188
|
+
expect(diags).toHaveLength(1);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("an unregistered call still flags with the registry present", () => {
|
|
192
|
+
const ctx: LintContext = { ...createContext(`new Bucket({ name: makeName() });`), intrinsics: REF };
|
|
193
|
+
const diags = evl001NonLiteralExpressionRule.check(ctx);
|
|
194
|
+
expect(diags).toHaveLength(1);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
158
197
|
});
|
|
@@ -31,7 +31,7 @@ function checkNode(node: ts.Node, context: LintContext, diagnostics: LintDiagnos
|
|
|
31
31
|
const firstArg = node.arguments[0];
|
|
32
32
|
if (ts.isObjectLiteralExpression(firstArg)) {
|
|
33
33
|
for (const prop of firstArg.properties) {
|
|
34
|
-
const violation = checkObjectMember(prop);
|
|
34
|
+
const violation = checkObjectMember(prop, context.intrinsics);
|
|
35
35
|
if (violation) {
|
|
36
36
|
const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(
|
|
37
37
|
violation.node.getStart(context.sourceFile),
|
package/src/serializer-walker.ts
CHANGED
|
@@ -61,6 +61,20 @@ export function walkValue(
|
|
|
61
61
|
if (name) {
|
|
62
62
|
return visitor.resourceRef(name);
|
|
63
63
|
}
|
|
64
|
+
// A resource-kind Declarable constructed inline rather than exported as
|
|
65
|
+
// its own top-level entity (e.g. K8s `new PersistentVolumeClaim({...})`
|
|
66
|
+
// embedded directly in a StatefulSet's `volumeClaimTemplates`) has no
|
|
67
|
+
// logical name to Ref — it was never a key in `entities`, so
|
|
68
|
+
// resolveAttrRefs() never assigns one. Falling through to the generic
|
|
69
|
+
// "object" branch below would walk the Declarable's own enumerable
|
|
70
|
+
// properties, which for a Declarable are its self-referencing attribute
|
|
71
|
+
// accessors (AttrRef instances whose parent is the Declarable itself,
|
|
72
|
+
// still unresolved) — never its authored `.props`. That either threw
|
|
73
|
+
// "logical name not set" (when the resource type declares any
|
|
74
|
+
// attributes) or silently serialized as `{}` (when it doesn't), instead
|
|
75
|
+
// of the embedded spec the caller wrote. Embed its own props inline
|
|
76
|
+
// instead, exactly like a property-kind Declarable already does.
|
|
77
|
+
return visitor.propertyDeclarable(decl, (v) => walkValue(v, entityNames, visitor));
|
|
64
78
|
}
|
|
65
79
|
|
|
66
80
|
// Handle arrays
|