@intentius/chant 0.121.1 → 0.122.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/cli/commands/build.d.ts +12 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts +16 -0
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/build.ts +37 -18
- package/src/cli/commands/carve-emit-state.test.ts +8 -2
- package/src/cli/commands/lint.test.ts +109 -1
- package/src/cli/commands/lint.ts +162 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { build } from "../../build.js";
|
|
2
2
|
import type { Serializer } from "../../serializer.js";
|
|
3
3
|
import type { LexiconPlugin } from "../../lexicon.js";
|
|
4
|
+
import { runPostSynthChecks, type PostSynthDiagnostic } from "../../lint/post-synth.js";
|
|
4
5
|
import { type FoldRankResult } from "../../discovery/fold-rank.js";
|
|
5
6
|
/**
|
|
6
7
|
* Build command options
|
|
@@ -109,6 +110,17 @@ export interface BuildResult {
|
|
|
109
110
|
/** This build's resolved build-time parameters (#1064) — see `BuildResult.buildParams` (../../build.ts). Empty when the project declares/supplies none. */
|
|
110
111
|
buildParams?: import("../../provenance.js").BuildParamProvenance[];
|
|
111
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Run each lexicon plugin's own post-synth checks over a build result, each
|
|
115
|
+
* plugin seeing only its own lexicon's output. Shared by `chant build` and
|
|
116
|
+
* `chant lint` (#3750), so the two run the same checks over the same scoping;
|
|
117
|
+
* each caller then applies `lint.presets`, `lint.rules` and inline
|
|
118
|
+
* suppressions its own way. A plugin with no checks yields no entry.
|
|
119
|
+
*/
|
|
120
|
+
export declare function runLexiconPostSynthChecks(plugins: readonly LexiconPlugin[], result: Parameters<typeof runPostSynthChecks>[1], env: string | undefined, opts?: Parameters<typeof runPostSynthChecks>[3]): Array<{
|
|
121
|
+
plugin: LexiconPlugin;
|
|
122
|
+
diagnostics: PostSynthDiagnostic[];
|
|
123
|
+
}>;
|
|
112
124
|
/**
|
|
113
125
|
* Execute the build command
|
|
114
126
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/build.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAepC,OAAO,KAAK,EAAE,UAAU,EAAoB,MAAM,kBAAkB,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/build.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAepC,OAAO,KAAK,EAAE,UAAU,EAAoB,MAAM,kBAAkB,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,kBAAkB,EAAE,KAAK,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAgBrF,OAAO,EAAuC,KAAK,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAMrG;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oBAAoB;IACpB,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,2CAA2C;IAC3C,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,8CAA8C;IAC9C,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;IAC1B,8BAA8B;IAC9B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IAEf;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEhC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAE/B;;;;;OAKG;IACH,WAAW,CAAC,EAAE;QAAE,WAAW,EAAE,UAAU,EAAE,CAAC;QAAC,MAAM,CAAC,EAAE,UAAU,CAAC,OAAO,KAAK,CAAC,CAAA;KAAE,CAAC;CAChF;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,GAAG,SAAS,GACzB;IAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAoB/C;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,kCAAkC;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,gCAAgC;IAChC,aAAa,EAAE,MAAM,CAAC;IACtB,uCAAuC;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,qBAAqB;IACrB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,uBAAuB;IACvB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,2JAA2J;IAC3J,WAAW,CAAC,EAAE,OAAO,kBAAkB,EAAE,oBAAoB,EAAE,CAAC;CACjE;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,SAAS,aAAa,EAAE,EACjC,MAAM,EAAE,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC,CAAC,CAAC,EAChD,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,IAAI,CAAC,EAAE,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC,CAAC,CAAC,GAC9C,KAAK,CAAC;IAAE,MAAM,EAAE,aAAa,CAAC;IAAC,WAAW,EAAE,mBAAmB,EAAE,CAAA;CAAE,CAAC,CAsBtE;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA0vB9E;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,SAAS;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,GAAG,MAAM,CAMrF;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAsBpF;AAiDD;;GAEG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,CAIlD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAI,CAItD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,YAAY,EACrB,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,GACxC,MAAM,IAAI,CAmCZ"}
|
|
@@ -42,6 +42,15 @@ export declare function loadPluginRules(plugins: string[], configDir: string): P
|
|
|
42
42
|
* project" this id exists to prevent.
|
|
43
43
|
*/
|
|
44
44
|
export declare const LEXICON_RESOLUTION_RULE_ID = "LEX001";
|
|
45
|
+
/**
|
|
46
|
+
* The diagnostic id `chant lint` reports a lint target that does not build
|
|
47
|
+
* under (chant #3750). The lexicons' post-synth checks read a build result, so
|
|
48
|
+
* when the build fails they cannot run; this says so instead of letting the
|
|
49
|
+
* lint pass in silence. Like {@link LEXICON_RESOLUTION_RULE_ID} it is not a
|
|
50
|
+
* rule: it has no severity setting and no disable form. The message is the
|
|
51
|
+
* build's own.
|
|
52
|
+
*/
|
|
53
|
+
export declare const BUILD_FAILURE_RULE_ID = "LEX002";
|
|
45
54
|
/**
|
|
46
55
|
* Lint command options
|
|
47
56
|
*/
|
|
@@ -70,6 +79,13 @@ export interface LintOptions {
|
|
|
70
79
|
* `sandbox` option (../../components/discover.ts).
|
|
71
80
|
*/
|
|
72
81
|
sandbox?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* chant #3750 — run each loaded lexicon's post-synth checks over an
|
|
84
|
+
* in-memory build of the target (default `true`, what the CLI always does).
|
|
85
|
+
* `false` lints source alone; it exists for test harnesses that check an
|
|
86
|
+
* example's source rules, not for hiding findings from a user.
|
|
87
|
+
*/
|
|
88
|
+
postSynth?: boolean;
|
|
73
89
|
}
|
|
74
90
|
/**
|
|
75
91
|
* Lint command result
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lint.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/lint.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAG7D,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAW,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"lint.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/lint.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAG7D,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAW,MAAM,iBAAiB,CAAC;AAsCzE;;GAEG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,QAAQ,CAW5D;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,MAAM,EAAE,EACjB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAmBhC;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,WAAW,CAAC;AAsBnD;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,WAAW,CAAC;AA2I9C;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,oBAAoB,EAAE,CAAC;IACrC,uBAAuB;IACvB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,oBAAoB;IACpB,MAAM,EAAE,SAAS,GAAG,MAAM,GAAG,OAAO,CAAC;IACrC,qCAAqC;IACrC,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAC;IACnB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,sCAAsC;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,uBAAuB;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,yBAAyB;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,sBAAsB;IACtB,WAAW,EAAE,cAAc,EAAE,CAAC;IAC9B,uBAAuB;IACvB,MAAM,EAAE,MAAM,CAAC;CAChB;AA+cD;;GAEG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CA+N3E;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAIxD;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,GACtC,MAAM,IAAI,CAiCZ"}
|
package/package.json
CHANGED
|
@@ -170,6 +170,42 @@ export interface BuildResult {
|
|
|
170
170
|
buildParams?: import("../../provenance").BuildParamProvenance[];
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Run each lexicon plugin's own post-synth checks over a build result, each
|
|
175
|
+
* plugin seeing only its own lexicon's output. Shared by `chant build` and
|
|
176
|
+
* `chant lint` (#3750), so the two run the same checks over the same scoping;
|
|
177
|
+
* each caller then applies `lint.presets`, `lint.rules` and inline
|
|
178
|
+
* suppressions its own way. A plugin with no checks yields no entry.
|
|
179
|
+
*/
|
|
180
|
+
export function runLexiconPostSynthChecks(
|
|
181
|
+
plugins: readonly LexiconPlugin[],
|
|
182
|
+
result: Parameters<typeof runPostSynthChecks>[1],
|
|
183
|
+
env: string | undefined,
|
|
184
|
+
opts?: Parameters<typeof runPostSynthChecks>[3],
|
|
185
|
+
): Array<{ plugin: LexiconPlugin; diagnostics: PostSynthDiagnostic[] }> {
|
|
186
|
+
const batches: Array<{ plugin: LexiconPlugin; diagnostics: PostSynthDiagnostic[] }> = [];
|
|
187
|
+
for (const plugin of plugins) {
|
|
188
|
+
if (!plugin.postSynthChecks) continue;
|
|
189
|
+
const checks = plugin.postSynthChecks();
|
|
190
|
+
if (checks.length === 0) continue;
|
|
191
|
+
|
|
192
|
+
// Scope outputs to this plugin's lexicon so cross-lexicon outputs don't
|
|
193
|
+
// interfere. Outputs are keyed by the serializer's lexicon name (the
|
|
194
|
+
// build partition key), which differs from plugin.name for a dialect like
|
|
195
|
+
// forgejo (plugin "forgejo", serializer "github").
|
|
196
|
+
const outputKey = plugin.serializer.name;
|
|
197
|
+
const scopedOutputs = new Map<string, string | SerializerResult>();
|
|
198
|
+
const pluginOutput = result.outputs.get(outputKey);
|
|
199
|
+
if (pluginOutput !== undefined) {
|
|
200
|
+
scopedOutputs.set(outputKey, pluginOutput);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const scopedResult = { ...result, outputs: scopedOutputs };
|
|
204
|
+
batches.push({ plugin, diagnostics: runPostSynthChecks(checks, scopedResult, env, opts) });
|
|
205
|
+
}
|
|
206
|
+
return batches;
|
|
207
|
+
}
|
|
208
|
+
|
|
173
209
|
/**
|
|
174
210
|
* Execute the build command
|
|
175
211
|
*/
|
|
@@ -549,24 +585,7 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
549
585
|
}
|
|
550
586
|
|
|
551
587
|
if (result.errors.length === 0 && options.plugins) {
|
|
552
|
-
for (const plugin of options.plugins) {
|
|
553
|
-
if (!plugin.postSynthChecks) continue;
|
|
554
|
-
const checks = plugin.postSynthChecks();
|
|
555
|
-
if (checks.length === 0) continue;
|
|
556
|
-
|
|
557
|
-
// Scope outputs to this plugin's lexicon so cross-lexicon outputs don't
|
|
558
|
-
// interfere. Outputs are keyed by the serializer's lexicon name (the
|
|
559
|
-
// build partition key), which differs from plugin.name for a dialect like
|
|
560
|
-
// forgejo (plugin "forgejo", serializer "github").
|
|
561
|
-
const outputKey = plugin.serializer.name;
|
|
562
|
-
const scopedOutputs = new Map<string, string | SerializerResult>();
|
|
563
|
-
const pluginOutput = result.outputs.get(outputKey);
|
|
564
|
-
if (pluginOutput !== undefined) {
|
|
565
|
-
scopedOutputs.set(outputKey, pluginOutput);
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
const scopedResult = { ...result, outputs: scopedOutputs };
|
|
569
|
-
const postDiags = runPostSynthChecks(checks, scopedResult, env, { activityContracts, telemetry });
|
|
588
|
+
for (const { plugin, diagnostics: postDiags } of runLexiconPostSynthChecks(options.plugins, result, env, { activityContracts, telemetry })) {
|
|
570
589
|
// `lint.presets` (chant #2113) filters WHICH check ids are reported at
|
|
571
590
|
// all, before `lint.rules`/inline suppression act on the reported
|
|
572
591
|
// ones. It's a no-op for a lexicon that ships no `lintPresets()` (the
|
|
@@ -317,9 +317,15 @@ resource "aws_s3_bucket_public_access_block" "assets" {
|
|
|
317
317
|
expect(text).toContain("aws_s3_bucket_versioning.assets (VersioningConfiguration)");
|
|
318
318
|
expect(text).toContain("aws_s3_bucket_public_access_block.assets (PublicAccessBlockConfiguration)");
|
|
319
319
|
|
|
320
|
-
// And the emitted project still lints clean (no errors).
|
|
320
|
+
// And the emitted project still lints clean (no errors). Since #3750 the
|
|
321
|
+
// lint runs the aws post-synth checks too, and WAW042 is expected here:
|
|
322
|
+
// the Terraform declares no bucket policy, so the carved bucket faithfully
|
|
323
|
+
// has no TLS-only deny either. The carve reproduces what was declared; it
|
|
324
|
+
// does not add policy, so that one finding is allowed and nothing else.
|
|
321
325
|
const lint = await lintCommand({ path: out, format: "stylish" });
|
|
322
|
-
|
|
326
|
+
const errors = lint.diagnostics.filter((d) => d.severity === "error");
|
|
327
|
+
expect(errors.filter((d) => d.ruleId !== "WAW042")).toEqual([]);
|
|
328
|
+
expect(errors.map((d) => d.ruleId)).toEqual(["WAW042"]);
|
|
323
329
|
} finally {
|
|
324
330
|
rmSync(dir, { recursive: true, force: true });
|
|
325
331
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
-
import { lintCommand, isLintRule, loadPluginRules, LEXICON_RESOLUTION_RULE_ID, type LintOptions } from "./lint";
|
|
2
|
+
import { lintCommand, isLintRule, loadPluginRules, LEXICON_RESOLUTION_RULE_ID, BUILD_FAILURE_RULE_ID, type LintOptions } from "./lint";
|
|
3
3
|
import { loadPlugins, resolveProjectLexicons } from "../plugins";
|
|
4
4
|
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
5
5
|
import { join, resolve } from "node:path";
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
7
7
|
import { execFileSync } from "node:child_process";
|
|
8
|
+
import { mkdtempSync, realpathSync, symlinkSync } from "node:fs";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
8
10
|
import { params, setBuildParams } from "../../params";
|
|
9
11
|
|
|
10
12
|
describe("lintCommand", () => {
|
|
@@ -959,3 +961,109 @@ describe("lintCommand build-time parameters (#2249)", () => {
|
|
|
959
961
|
expect(params).toEqual({});
|
|
960
962
|
});
|
|
961
963
|
});
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* chant #3750 — `chant lint` runs each loaded lexicon's own post-synth checks
|
|
967
|
+
* over an in-memory build of the lint target, the ones `chant build` runs.
|
|
968
|
+
* The sql lexicon's SQLCH101 is the issue's repro: `ENGINE = MergeTre` failed
|
|
969
|
+
* the build and linted clean. The project imports the workspace's sql
|
|
970
|
+
* lexicon through a `node_modules` link, as an installed project would.
|
|
971
|
+
*/
|
|
972
|
+
describe("lintCommand — lexicon post-synth checks (#3750)", () => {
|
|
973
|
+
const repoNodeModules = resolve(fileURLToPath(new URL(".", import.meta.url)), "../../../../../node_modules");
|
|
974
|
+
let root: string;
|
|
975
|
+
|
|
976
|
+
const schema = (engine: string, idType = "UInt64") =>
|
|
977
|
+
'import { database, table } from "@intentius/chant-lexicon-sql/clickhouse";\n' +
|
|
978
|
+
"export const shopDb = database`CREATE DATABASE shop ENGINE = Atomic`;\n" +
|
|
979
|
+
"export const events = table`\n" +
|
|
980
|
+
` CREATE TABLE \${shopDb}.events (id ${idType}, kind String)\n` +
|
|
981
|
+
` ENGINE = ${engine}\n` +
|
|
982
|
+
" ORDER BY (kind, id)`;\n";
|
|
983
|
+
|
|
984
|
+
async function project(engine: string, rules?: Record<string, string>, idType?: string): Promise<void> {
|
|
985
|
+
await writeFile(
|
|
986
|
+
join(root, "chant.config.json"),
|
|
987
|
+
JSON.stringify({ lexicons: ["sql"], sql: { dialect: "clickhouse" }, ...(rules ? { lint: { rules } } : {}) }),
|
|
988
|
+
);
|
|
989
|
+
await writeFile(join(root, "src", "schema.ts"), schema(engine, idType));
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
beforeEach(async () => {
|
|
993
|
+
root = realpathSync(mkdtempSync(join(tmpdir(), "chant-3750-")));
|
|
994
|
+
await mkdir(join(root, "src"), { recursive: true });
|
|
995
|
+
symlinkSync(repoNodeModules, join(root, "node_modules"), "dir");
|
|
996
|
+
process.env.NO_COLOR = "1";
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
afterEach(async () => {
|
|
1000
|
+
await rm(root, { recursive: true, force: true });
|
|
1001
|
+
delete process.env.NO_COLOR;
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
test("a lexicon post-synth error fails the lint, at the project root and at src", async () => {
|
|
1005
|
+
await project("MergeTre");
|
|
1006
|
+
for (const path of [root, join(root, "src")]) {
|
|
1007
|
+
const result = await lintCommand({ path, format: "stylish" });
|
|
1008
|
+
expect(result.success).toBe(false);
|
|
1009
|
+
const diag = result.diagnostics.find((d) => d.ruleId === "SQLCH101");
|
|
1010
|
+
expect(diag, result.output).toBeDefined();
|
|
1011
|
+
expect(diag!.severity).toBe("error");
|
|
1012
|
+
expect(diag!.message).toContain('"MergeTre"');
|
|
1013
|
+
// No line of its own: reported at 1:1 in the file that declared the table.
|
|
1014
|
+
expect(diag!.file).toBe(join(root, "src", "schema.ts"));
|
|
1015
|
+
expect([diag!.line, diag!.column]).toEqual([1, 1]);
|
|
1016
|
+
expect(result.output).toContain("SQLCH101");
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
test("a column type the pinned server does not have is reported (SQLCH121)", async () => {
|
|
1021
|
+
await project("MergeTree", undefined, "UInt46");
|
|
1022
|
+
const result = await lintCommand({ path: root, format: "stylish" });
|
|
1023
|
+
expect(result.success).toBe(false);
|
|
1024
|
+
const diag = result.diagnostics.find((d) => d.ruleId === "SQLCH121");
|
|
1025
|
+
expect(diag, result.output).toBeDefined();
|
|
1026
|
+
expect(diag!.message).toContain("UInt46");
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
test("postSynth: false lints source alone", async () => {
|
|
1030
|
+
await project("MergeTre");
|
|
1031
|
+
const result = await lintCommand({ path: root, format: "stylish", postSynth: false });
|
|
1032
|
+
expect(result.diagnostics.some((d) => d.ruleId === "SQLCH101")).toBe(false);
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
test("a clean project reports nothing from the post-synth pass", async () => {
|
|
1036
|
+
await project("MergeTree");
|
|
1037
|
+
const result = await lintCommand({ path: root, format: "stylish" });
|
|
1038
|
+
expect(result.diagnostics.filter((d) => /^SQL|^LEX/.test(d.ruleId)), result.output).toEqual([]);
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
test("lint.rules overrides a post-synth finding's severity, and turns it off", async () => {
|
|
1042
|
+
await project("MergeTre", { SQLCH101: "warning" });
|
|
1043
|
+
const warned = await lintCommand({ path: root, format: "stylish" });
|
|
1044
|
+
expect(warned.diagnostics.find((d) => d.ruleId === "SQLCH101")?.severity).toBe("warning");
|
|
1045
|
+
expect(warned.success).toBe(true);
|
|
1046
|
+
|
|
1047
|
+
await project("MergeTre", { SQLCH101: "off" });
|
|
1048
|
+
const off = await lintCommand({ path: root, format: "stylish" });
|
|
1049
|
+
expect(off.diagnostics.some((d) => d.ruleId === "SQLCH101")).toBe(false);
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
test("a target that does not build reports the build error and skips the checks", async () => {
|
|
1053
|
+
await project("MergeTre");
|
|
1054
|
+
// A table interpolating a name that is not declared does not build.
|
|
1055
|
+
await writeFile(
|
|
1056
|
+
join(root, "src", "broken.ts"),
|
|
1057
|
+
'import { table } from "@intentius/chant-lexicon-sql/clickhouse";\n' +
|
|
1058
|
+
'import { events } from "./schema";\n' +
|
|
1059
|
+
"export const copy = table`CREATE TABLE shop.copy (id UInt64) ENGINE = MergeTree ORDER BY ${(events as any).columns.nope}`;\n",
|
|
1060
|
+
);
|
|
1061
|
+
const result = await lintCommand({ path: root, format: "stylish" });
|
|
1062
|
+
expect(result.success).toBe(false);
|
|
1063
|
+
const failures = result.diagnostics.filter((d) => d.ruleId === BUILD_FAILURE_RULE_ID);
|
|
1064
|
+
expect(failures.length, result.output).toBeGreaterThan(0);
|
|
1065
|
+
expect(failures.every((d) => d.severity === "error")).toBe(true);
|
|
1066
|
+
expect(failures.map((d) => d.message).join("\n")).toContain("is undefined");
|
|
1067
|
+
expect(result.diagnostics.some((d) => d.ruleId === "SQLCH101")).toBe(false);
|
|
1068
|
+
});
|
|
1069
|
+
});
|
package/src/cli/commands/lint.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolve, join, relative } from "path";
|
|
1
|
+
import { resolve, join, relative, dirname } from "path";
|
|
2
2
|
import type { BuildParamProvenance } from "../../provenance";
|
|
3
3
|
import { readFileSync, writeFileSync, readdirSync } from "fs";
|
|
4
4
|
import { runLint, parseDisableComments } from "../../lint/engine";
|
|
@@ -24,6 +24,14 @@ import { walkDiscovery, workspaceMemberDirs } from "../../discovery/walk";
|
|
|
24
24
|
import { buildParamValues, resolveBuildParams } from "../../build-params";
|
|
25
25
|
import { setBuildParams } from "../../params";
|
|
26
26
|
import { isNoLexiconDetected } from "../../detectLexicon";
|
|
27
|
+
import { build } from "../../build";
|
|
28
|
+
import { loadChantConfigUpward, resolveOwnershipEnv, resolveOwnershipMarker } from "../../config";
|
|
29
|
+
import { resolveBuildModes, resolveProjectBuildOptions } from "../build-options";
|
|
30
|
+
import { resolveTelemetryAttribution } from "../../telemetry-attribution";
|
|
31
|
+
import { applyConfiguredPreset, applyConfiguredSeverity, resolvePresetIds } from "../../lint/config";
|
|
32
|
+
import { applyInlineSuppressions } from "../../lint/suppressions";
|
|
33
|
+
import { getProvenance } from "../../provenance";
|
|
34
|
+
import { runLexiconPostSynthChecks } from "./build";
|
|
27
35
|
|
|
28
36
|
// Import config loader
|
|
29
37
|
import { loadConfig, resolveRulesForFile, resolveConfiguredSeverity, findProjectRoot } from "../../lint/config";
|
|
@@ -130,6 +138,16 @@ function lexiconResolutionDiagnostic(projectRoot: string, error: Error): LintDia
|
|
|
130
138
|
};
|
|
131
139
|
}
|
|
132
140
|
|
|
141
|
+
/**
|
|
142
|
+
* The diagnostic id `chant lint` reports a lint target that does not build
|
|
143
|
+
* under (chant #3750). The lexicons' post-synth checks read a build result, so
|
|
144
|
+
* when the build fails they cannot run; this says so instead of letting the
|
|
145
|
+
* lint pass in silence. Like {@link LEXICON_RESOLUTION_RULE_ID} it is not a
|
|
146
|
+
* rule: it has no severity setting and no disable form. The message is the
|
|
147
|
+
* build's own.
|
|
148
|
+
*/
|
|
149
|
+
export const BUILD_FAILURE_RULE_ID = "LEX002";
|
|
150
|
+
|
|
133
151
|
/**
|
|
134
152
|
* The plugins of lexicons that the project's source imports from and that are not in `configured`, for their
|
|
135
153
|
* property-kind class names (#3184). A package that is not installed, or that fails to load, is skipped.
|
|
@@ -295,6 +313,13 @@ export interface LintOptions {
|
|
|
295
313
|
* `sandbox` option (../../components/discover.ts).
|
|
296
314
|
*/
|
|
297
315
|
sandbox?: boolean;
|
|
316
|
+
/**
|
|
317
|
+
* chant #3750 — run each loaded lexicon's post-synth checks over an
|
|
318
|
+
* in-memory build of the target (default `true`, what the CLI always does).
|
|
319
|
+
* `false` lints source alone; it exists for test harnesses that check an
|
|
320
|
+
* example's source rules, not for hiding findings from a user.
|
|
321
|
+
*/
|
|
322
|
+
postSynth?: boolean;
|
|
298
323
|
}
|
|
299
324
|
|
|
300
325
|
/**
|
|
@@ -630,6 +655,132 @@ async function runOpCheckDiagnostics(
|
|
|
630
655
|
return { diagnostics, suppressed };
|
|
631
656
|
}
|
|
632
657
|
|
|
658
|
+
/**
|
|
659
|
+
* Run each loaded lexicon's own post-synth checks (#3750), the ones `chant
|
|
660
|
+
* build` runs after it serializes, over an in-memory build of the lint target.
|
|
661
|
+
*
|
|
662
|
+
* The build is `chant build`'s: the same `build()` call with options from the
|
|
663
|
+
* same assembler (`../build-options.ts`), and the same per-lexicon scoping of
|
|
664
|
+
* outputs (`runLexiconPostSynthChecks`, ./build.ts). Nothing is written. It
|
|
665
|
+
* builds `infraPath`, the target the caller asked to lint, never the project
|
|
666
|
+
* root above it, so `chant lint <dir>` costs a build of `<dir>` and nothing
|
|
667
|
+
* more (the same scoping {@link runOpCheckDiagnostics} documents). It is
|
|
668
|
+
* skipped outright when no loaded lexicon ships post-synth checks, which keeps
|
|
669
|
+
* a project with no lexicon, and every core-only fixture, from building at
|
|
670
|
+
* all.
|
|
671
|
+
*
|
|
672
|
+
* A target that does not build reports each build error under
|
|
673
|
+
* {@link BUILD_FAILURE_RULE_ID} and skips the checks. Findings go through
|
|
674
|
+
* `lint.presets`, then `lint.rules`, then inline `chant-ignore` comments, the
|
|
675
|
+
* order `chant build` applies them in. A finding carries no line (see
|
|
676
|
+
* ../../lint/post-synth.ts's `PostSynthDiagnostic`), so it is reported at
|
|
677
|
+
* `1:1` in the file that declared its entity, or at the config file (the
|
|
678
|
+
* project root without one) when it names none.
|
|
679
|
+
*/
|
|
680
|
+
async function runLexiconPostSynthDiagnostics(
|
|
681
|
+
infraPath: string,
|
|
682
|
+
plugins: readonly LexiconPlugin[],
|
|
683
|
+
buildParams?: BuildParamProvenance[],
|
|
684
|
+
sandbox?: boolean,
|
|
685
|
+
): Promise<{ diagnostics: LintDiagnostic[]; suppressed: Array<LintDiagnostic & { reason?: string }> }> {
|
|
686
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
687
|
+
const suppressed: Array<LintDiagnostic & { reason?: string }> = [];
|
|
688
|
+
const withChecks = plugins.filter((plugin) => (plugin.postSynthChecks?.() ?? []).length > 0);
|
|
689
|
+
if (withChecks.length === 0) return { diagnostics, suppressed };
|
|
690
|
+
|
|
691
|
+
const projectRoot = findProjectRoot(infraPath);
|
|
692
|
+
const lintConfig = loadConfig(projectRoot);
|
|
693
|
+
// Where a finding that names no declared entity is reported: the config the
|
|
694
|
+
// build loads once it is known (past a lint-only fragment), else the nearest.
|
|
695
|
+
let fallbackFile = findProjectConfig(projectRoot).configPath ?? projectRoot;
|
|
696
|
+
const buildFailure = (message: string, at?: { file?: string; line?: number; column?: number }): LintDiagnostic => ({
|
|
697
|
+
file: at?.file ?? fallbackFile,
|
|
698
|
+
line: at?.line ?? 1,
|
|
699
|
+
column: at?.column ?? 1,
|
|
700
|
+
ruleId: BUILD_FAILURE_RULE_ID,
|
|
701
|
+
severity: "error",
|
|
702
|
+
message,
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
let result: Awaited<ReturnType<typeof build>>;
|
|
706
|
+
let env: string | undefined;
|
|
707
|
+
let telemetry: Awaited<ReturnType<typeof resolveTelemetryAttribution>>;
|
|
708
|
+
try {
|
|
709
|
+
const loaded = await loadChantConfigUpward(infraPath);
|
|
710
|
+
const config = loaded.config;
|
|
711
|
+
const configDir = loaded.configPath ? dirname(loaded.configPath) : infraPath;
|
|
712
|
+
if (loaded.configPath) fallbackFile = loaded.configPath;
|
|
713
|
+
// The caller's parameters when it resolved them (the CLI does, from the
|
|
714
|
+
// same upward config walk); otherwise the declared defaults and `env`
|
|
715
|
+
// mappings of the config this build loads. Not lintCommand's own fallback:
|
|
716
|
+
// that one reads the config at the lint root, which may be a lint-only
|
|
717
|
+
// fragment with no `buildParams` (a `src/chant.config.json`).
|
|
718
|
+
const params = buildParams ?? resolveBuildParams(config.buildParams, { env: process.env }).provenance;
|
|
719
|
+
env = resolveOwnershipEnv(config, params);
|
|
720
|
+
telemetry = await resolveTelemetryAttribution(configDir, config as unknown as Record<string, unknown>, env);
|
|
721
|
+
// Every lexicon the lint loaded, as `chant build` serializes with every
|
|
722
|
+
// lexicon it loaded: a check may read another lexicon's entities, and the
|
|
723
|
+
// outputs are scoped per plugin below either way.
|
|
724
|
+
result = await build(
|
|
725
|
+
infraPath,
|
|
726
|
+
plugins.map((plugin) => plugin.serializer),
|
|
727
|
+
undefined,
|
|
728
|
+
resolveProjectBuildOptions({
|
|
729
|
+
config,
|
|
730
|
+
configDir,
|
|
731
|
+
plugins,
|
|
732
|
+
modes: resolveBuildModes(config, { sandbox }),
|
|
733
|
+
ownership: resolveOwnershipMarker(config, params),
|
|
734
|
+
buildParams: params,
|
|
735
|
+
telemetry,
|
|
736
|
+
}),
|
|
737
|
+
);
|
|
738
|
+
} catch (err) {
|
|
739
|
+
diagnostics.push(buildFailure(err instanceof Error ? err.message : String(err)));
|
|
740
|
+
return { diagnostics, suppressed };
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (result.errors.length > 0) {
|
|
744
|
+
const seen = new Set<string>();
|
|
745
|
+
for (const error of result.errors) {
|
|
746
|
+
const at = error as unknown as { file?: string; line?: number; column?: number };
|
|
747
|
+
const key = `${at.file ?? ""}:${at.line ?? ""}:${error.message}`;
|
|
748
|
+
if (seen.has(key)) continue;
|
|
749
|
+
seen.add(key);
|
|
750
|
+
diagnostics.push(buildFailure(error.message, at));
|
|
751
|
+
}
|
|
752
|
+
return { diagnostics, suppressed };
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const activityContracts = await loadActivityContracts(plugins);
|
|
756
|
+
const fileOf = (entity?: string): string => {
|
|
757
|
+
const declared = entity ? result.entities.get(entity) : undefined;
|
|
758
|
+
return (declared && getProvenance(declared)?.sourceFile) || fallbackFile;
|
|
759
|
+
};
|
|
760
|
+
const toLint = (d: { checkId: string; severity: LintDiagnostic["severity"]; message: string; entity?: string; lexicon?: string }): LintDiagnostic => ({
|
|
761
|
+
file: fileOf(d.entity),
|
|
762
|
+
line: 1,
|
|
763
|
+
column: 1,
|
|
764
|
+
ruleId: d.checkId,
|
|
765
|
+
severity: d.severity,
|
|
766
|
+
message: `${d.entity ? `[${d.entity}] ` : ""}${d.message}`,
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
for (const { plugin, diagnostics: raw } of runLexiconPostSynthChecks(withChecks, result, env, { activityContracts, telemetry })) {
|
|
770
|
+
const presetIds = resolvePresetIds(plugin.lintPresets?.(), lintConfig.presets?.[plugin.name] ?? "recommended");
|
|
771
|
+
const afterPreset = applyConfiguredPreset(raw, presetIds, lintConfig.rules).diagnostics;
|
|
772
|
+
const afterConfig = applyConfiguredSeverity(afterPreset, lintConfig.rules).diagnostics;
|
|
773
|
+
const inline = applyInlineSuppressions(afterConfig, result.entities, lintConfig.rules);
|
|
774
|
+
for (const d of inline.diagnostics) diagnostics.push(toLint(d));
|
|
775
|
+
for (const d of inline.suppressed) suppressed.push({ ...toLint(d), reason: "chant-ignore" });
|
|
776
|
+
for (const m of inline.meta) {
|
|
777
|
+
diagnostics.push({ file: m.file, line: m.line, column: 1, ruleId: m.checkId, severity: m.severity, message: m.message });
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
return { diagnostics, suppressed };
|
|
782
|
+
}
|
|
783
|
+
|
|
633
784
|
/**
|
|
634
785
|
* Apply fixes to a file
|
|
635
786
|
*/
|
|
@@ -817,6 +968,16 @@ export async function lintCommand(options: LintOptions): Promise<LintResult> {
|
|
|
817
968
|
suppressed.push(...postOpResult.suppressed);
|
|
818
969
|
}
|
|
819
970
|
|
|
971
|
+
// chant #3750: each lexicon's own post-synth checks, over an in-memory
|
|
972
|
+
// build of the lint target. Run once, after any `--fix` above, so it sees
|
|
973
|
+
// the source as fixed; a post-synth finding has no fix of its own. Skipped
|
|
974
|
+
// when a lexicon failed to resolve, which LEX001 below already reports.
|
|
975
|
+
if (!loaded.lexiconError && options.postSynth !== false) {
|
|
976
|
+
const lexiconResult = await runLexiconPostSynthDiagnostics(infraPath, loaded.plugins, options.buildParams, options.sandbox);
|
|
977
|
+
diagnostics.push(...lexiconResult.diagnostics);
|
|
978
|
+
suppressed.push(...lexiconResult.suppressed);
|
|
979
|
+
}
|
|
980
|
+
|
|
820
981
|
// chant #2222: a lexicon the project declared but this run could not
|
|
821
982
|
// resolve. Appended here, after the `--fix` re-lint block above has finished
|
|
822
983
|
// reassigning `diagnostics`, so it survives every path; it is a property of
|