@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.
Files changed (57) hide show
  1. package/dist/build.d.ts +7 -0
  2. package/dist/build.d.ts.map +1 -1
  3. package/dist/cli/build-params-cli.d.ts +55 -0
  4. package/dist/cli/build-params-cli.d.ts.map +1 -0
  5. package/dist/cli/commands/build.d.ts.map +1 -1
  6. package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
  7. package/dist/cli/commands/lint.d.ts.map +1 -1
  8. package/dist/cli/handlers/build.d.ts.map +1 -1
  9. package/dist/cli/handlers/run.d.ts +14 -1
  10. package/dist/cli/handlers/run.d.ts.map +1 -1
  11. package/dist/cli/lsp/server.d.ts.map +1 -1
  12. package/dist/components/cli-support.d.ts +33 -2
  13. package/dist/components/cli-support.d.ts.map +1 -1
  14. package/dist/components/discover.d.ts +28 -0
  15. package/dist/components/discover.d.ts.map +1 -1
  16. package/dist/discovery/fold-import.d.ts +71 -9
  17. package/dist/discovery/fold-import.d.ts.map +1 -1
  18. package/dist/discovery/index.d.ts +27 -4
  19. package/dist/discovery/index.d.ts.map +1 -1
  20. package/dist/fold/fold.d.ts.map +1 -1
  21. package/dist/fold/subset.d.ts +39 -2
  22. package/dist/fold/subset.d.ts.map +1 -1
  23. package/dist/lint/engine.d.ts +11 -1
  24. package/dist/lint/engine.d.ts.map +1 -1
  25. package/dist/lint/rule.d.ts +14 -0
  26. package/dist/lint/rule.d.ts.map +1 -1
  27. package/dist/serializer-walker.d.ts.map +1 -1
  28. package/package.json +1 -1
  29. package/src/build.ts +9 -0
  30. package/src/cli/build-params-cli.test.ts +139 -0
  31. package/src/cli/build-params-cli.ts +107 -0
  32. package/src/cli/commands/build.ts +25 -36
  33. package/src/cli/commands/check-lexicon-examples.ts +16 -1
  34. package/src/cli/commands/lint.test.ts +74 -0
  35. package/src/cli/commands/lint.ts +33 -9
  36. package/src/cli/handlers/build.test.ts +147 -0
  37. package/src/cli/handlers/build.ts +23 -8
  38. package/src/cli/handlers/run.test.ts +160 -5
  39. package/src/cli/handlers/run.ts +46 -8
  40. package/src/cli/lsp/server.ts +7 -2
  41. package/src/components/cli-support.test.ts +221 -3
  42. package/src/components/cli-support.ts +37 -6
  43. package/src/components/discover.test.ts +63 -1
  44. package/src/components/discover.ts +42 -0
  45. package/src/discovery/fold-import.test.ts +328 -1
  46. package/src/discovery/fold-import.ts +414 -31
  47. package/src/discovery/index.test.ts +131 -0
  48. package/src/discovery/index.ts +53 -8
  49. package/src/discovery/sandbox/fold-boundary.test.ts +254 -0
  50. package/src/fold/fold.ts +6 -2
  51. package/src/fold/subset.test.ts +95 -15
  52. package/src/fold/subset.ts +46 -5
  53. package/src/lint/engine.ts +12 -0
  54. package/src/lint/rule.ts +14 -0
  55. package/src/lint/rules/evl001-non-literal-expression.test.ts +39 -0
  56. package/src/lint/rules/evl001-non-literal-expression.ts +1 -1
  57. package/src/serializer-walker.ts +14 -0
@@ -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; EVL is exactly such a caller and its behavior on calls
73
- * is unchanged by #1044.
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.getText()}`;
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.getText()}`;
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.getText()}(...)`;
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 {
@@ -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),
@@ -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