@rettangoli/check 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +295 -0
  2. package/ROADMAP.md +175 -0
  3. package/package.json +46 -0
  4. package/src/cli/bin.js +325 -0
  5. package/src/cli/check.js +232 -0
  6. package/src/cli/index.js +1 -0
  7. package/src/core/analyze.js +227 -0
  8. package/src/core/discovery.js +83 -0
  9. package/src/core/exportedFunctions.js +235 -0
  10. package/src/core/model.js +898 -0
  11. package/src/core/parsers.js +2726 -0
  12. package/src/core/registry.js +779 -0
  13. package/src/core/schema.js +161 -0
  14. package/src/core/scopeGraph.js +1400 -0
  15. package/src/core/semantic.js +329 -0
  16. package/src/diagnostics/autofix.js +191 -0
  17. package/src/diagnostics/catalog.js +89 -0
  18. package/src/index.js +2 -0
  19. package/src/reporters/index.js +13 -0
  20. package/src/reporters/json.js +42 -0
  21. package/src/reporters/sarif.js +213 -0
  22. package/src/reporters/text.js +145 -0
  23. package/src/rules/compatibility.js +318 -0
  24. package/src/rules/constants.js +22 -0
  25. package/src/rules/crossFileSymbols.js +108 -0
  26. package/src/rules/expression.js +338 -0
  27. package/src/rules/feParity.js +65 -0
  28. package/src/rules/index.js +39 -0
  29. package/src/rules/jempl.js +80 -0
  30. package/src/rules/lifecycle.js +4 -0
  31. package/src/rules/listenerConfig.js +556 -0
  32. package/src/rules/listenerSymbols.js +49 -0
  33. package/src/rules/methods.js +117 -0
  34. package/src/rules/refs.js +20 -0
  35. package/src/rules/schema.js +118 -0
  36. package/src/rules/shared.js +20 -0
  37. package/src/rules/yahtmlAttrs.js +238 -0
  38. package/src/semantic/engine.js +778 -0
  39. package/src/semantic/index.js +9 -0
  40. package/src/types/lattice.js +281 -0
  41. package/src/utils/case.js +9 -0
  42. package/src/utils/fs.js +30 -0
package/src/cli/bin.js ADDED
@@ -0,0 +1,325 @@
1
+ #!/usr/bin/env node
2
+ import check from "./check.js";
3
+ import { DIAGNOSTIC_CATALOG_VERSION, getDiagnosticCatalogEntry } from "../diagnostics/catalog.js";
4
+
5
+ const CHECK_USAGE = [
6
+ "Usage: rtgl-check [options]",
7
+ "",
8
+ "Options:",
9
+ " --dir <path> Component directory (repeatable)",
10
+ " --dirs <path> Alias for --dir",
11
+ " --format <value> Report format: text | json | sarif",
12
+ " --warn-as-error Treat warnings as errors",
13
+ " --no-yahtml Disable YAHTML attribute checks",
14
+ " --expr Enable template expression root checks",
15
+ " --autofix Apply safe autofixes in place",
16
+ " --autofix-dry-run Preview safe autofixes without writing files",
17
+ " --autofix-min-confidence <0-1> Minimum confidence threshold (default: 0.9)",
18
+ " --autofix-patch Include patch output for autofix candidates",
19
+ " --watch Re-run check when files change (polling)",
20
+ " --watch-interval-ms Poll interval in milliseconds (default: 800)",
21
+ " -h, --help Show this help",
22
+ ].join("\n");
23
+
24
+ class CliArgError extends Error {
25
+ constructor(message, { usage = CHECK_USAGE } = {}) {
26
+ super(message);
27
+ this.name = "CliArgError";
28
+ this.command = "check";
29
+ this.usage = usage;
30
+ }
31
+ }
32
+
33
+ const getRequiredValue = ({ args, index, flag, usage }) => {
34
+ const value = args[index + 1];
35
+ if (!value || value.startsWith("-")) {
36
+ throw new CliArgError(`Missing value for ${flag}.`, { usage });
37
+ }
38
+ return value;
39
+ };
40
+
41
+ const parseCheckArgs = (args = []) => {
42
+ if (args.includes("--help") || args.includes("-h")) {
43
+ return { help: true, usage: CHECK_USAGE, command: "check" };
44
+ }
45
+
46
+ const dirs = [];
47
+ let format = "text";
48
+ let warnAsError = false;
49
+ let includeYahtml = true;
50
+ let includeExpression = false;
51
+ let watch = false;
52
+ let watchIntervalMs = 800;
53
+ let autofixMode = "off";
54
+ let autofixMinConfidence = 0.9;
55
+ let autofixPatch = false;
56
+ let autofixMinConfidenceSet = false;
57
+
58
+ for (let i = 0; i < args.length; i += 1) {
59
+ const arg = args[i];
60
+
61
+ if (arg === "check") {
62
+ continue;
63
+ }
64
+
65
+ if (arg === "--dirs" || arg === "--dir") {
66
+ const value = getRequiredValue({ args, index: i, flag: arg, usage: CHECK_USAGE });
67
+ dirs.push(value);
68
+ i += 1;
69
+ continue;
70
+ }
71
+
72
+ if (arg === "--format") {
73
+ const value = getRequiredValue({ args, index: i, flag: arg, usage: CHECK_USAGE });
74
+ if (value !== "text" && value !== "json" && value !== "sarif") {
75
+ throw new CliArgError(`Invalid value for --format: "${value}". Expected "text", "json", or "sarif".`, {
76
+ usage: CHECK_USAGE,
77
+ });
78
+ }
79
+ format = value;
80
+ i += 1;
81
+ continue;
82
+ }
83
+
84
+ if (arg === "--warn-as-error") {
85
+ warnAsError = true;
86
+ continue;
87
+ }
88
+
89
+ if (arg === "--no-yahtml") {
90
+ includeYahtml = false;
91
+ continue;
92
+ }
93
+
94
+ if (arg === "--expr") {
95
+ includeExpression = true;
96
+ continue;
97
+ }
98
+
99
+ if (arg === "--autofix") {
100
+ if (autofixMode === "dry-run") {
101
+ throw new CliArgError("Cannot combine --autofix with --autofix-dry-run.", {
102
+ usage: CHECK_USAGE,
103
+ });
104
+ }
105
+ autofixMode = "apply";
106
+ continue;
107
+ }
108
+
109
+ if (arg === "--autofix-dry-run") {
110
+ if (autofixMode === "apply") {
111
+ throw new CliArgError("Cannot combine --autofix with --autofix-dry-run.", {
112
+ usage: CHECK_USAGE,
113
+ });
114
+ }
115
+ autofixMode = "dry-run";
116
+ continue;
117
+ }
118
+
119
+ if (arg === "--autofix-patch") {
120
+ autofixPatch = true;
121
+ if (autofixMode === "off") {
122
+ autofixMode = "dry-run";
123
+ }
124
+ continue;
125
+ }
126
+
127
+ if (arg === "--autofix-min-confidence") {
128
+ const value = getRequiredValue({ args, index: i, flag: arg, usage: CHECK_USAGE });
129
+ const parsed = Number.parseFloat(value);
130
+ if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
131
+ throw new CliArgError(`Invalid value for --autofix-min-confidence: "${value}". Expected a number in [0, 1].`, {
132
+ usage: CHECK_USAGE,
133
+ });
134
+ }
135
+ autofixMinConfidence = parsed;
136
+ autofixMinConfidenceSet = true;
137
+ i += 1;
138
+ continue;
139
+ }
140
+
141
+ if (arg === "--watch") {
142
+ watch = true;
143
+ continue;
144
+ }
145
+
146
+ if (arg === "--watch-interval-ms") {
147
+ const value = getRequiredValue({ args, index: i, flag: arg, usage: CHECK_USAGE });
148
+ const parsed = Number.parseInt(value, 10);
149
+ if (!Number.isFinite(parsed) || parsed <= 0) {
150
+ throw new CliArgError(`Invalid value for --watch-interval-ms: "${value}". Expected a positive integer.`, {
151
+ usage: CHECK_USAGE,
152
+ });
153
+ }
154
+ watchIntervalMs = parsed;
155
+ i += 1;
156
+ continue;
157
+ }
158
+
159
+ if (arg.startsWith("-")) {
160
+ throw new CliArgError(`Unknown option: ${arg}`, { usage: CHECK_USAGE });
161
+ }
162
+
163
+ throw new CliArgError(`Unexpected positional argument: ${arg}`, { usage: CHECK_USAGE });
164
+ }
165
+
166
+ if (autofixMode === "off" && autofixMinConfidenceSet) {
167
+ throw new CliArgError("Cannot use --autofix-min-confidence without --autofix or --autofix-dry-run.", {
168
+ usage: CHECK_USAGE,
169
+ });
170
+ }
171
+
172
+ return {
173
+ help: false,
174
+ usage: CHECK_USAGE,
175
+ command: "check",
176
+ options: {
177
+ cwd: process.cwd(),
178
+ dirs,
179
+ format,
180
+ warnAsError,
181
+ includeYahtml,
182
+ includeExpression,
183
+ watch,
184
+ watchIntervalMs,
185
+ autofixMode,
186
+ autofixMinConfidence,
187
+ autofixPatch,
188
+ },
189
+ };
190
+ };
191
+
192
+ const formatCliRuntimeErrorReport = ({ message, warnAsError = false }) => {
193
+ const catalogEntry = getDiagnosticCatalogEntry("RTGL-CLI-001");
194
+ const diagnostic = {
195
+ code: "RTGL-CLI-001",
196
+ category: "cli",
197
+ family: catalogEntry.family,
198
+ title: catalogEntry.title,
199
+ severity: "error",
200
+ message,
201
+ docsPath: catalogEntry.docsPath,
202
+ namespaceValid: catalogEntry.namespaceValid,
203
+ tags: catalogEntry.tags,
204
+ filePath: "unknown",
205
+ };
206
+
207
+ return JSON.stringify({
208
+ $schema: "docs/diagnostics-json-schema-v1.json",
209
+ schemaVersion: 1,
210
+ contractVersion: 1,
211
+ reportFormat: "json",
212
+ diagnosticCatalogVersion: DIAGNOSTIC_CATALOG_VERSION,
213
+ ok: false,
214
+ command: "check",
215
+ componentCount: 0,
216
+ registryTagCount: 0,
217
+ summary: {
218
+ total: 1,
219
+ bySeverity: {
220
+ error: 1,
221
+ warn: 0,
222
+ },
223
+ byCode: [
224
+ {
225
+ code: diagnostic.code,
226
+ count: 1,
227
+ },
228
+ ],
229
+ },
230
+ warnAsError,
231
+ diagnosticCatalog: [catalogEntry],
232
+ diagnostics: [diagnostic],
233
+ }, null, 2);
234
+ };
235
+
236
+ const formatCliRuntimeErrorSarif = ({ message }) => {
237
+ const catalogEntry = getDiagnosticCatalogEntry("RTGL-CLI-001");
238
+ return JSON.stringify({
239
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
240
+ version: "2.1.0",
241
+ runs: [
242
+ {
243
+ tool: {
244
+ driver: {
245
+ name: "@rettangoli/check",
246
+ rules: [
247
+ {
248
+ id: "RTGL-CLI-001",
249
+ shortDescription: { text: catalogEntry.title },
250
+ fullDescription: { text: catalogEntry.description },
251
+ helpUri: catalogEntry.docsPath,
252
+ defaultConfiguration: { level: "error" },
253
+ properties: {
254
+ category: catalogEntry.family,
255
+ namespaceValid: catalogEntry.namespaceValid,
256
+ tags: catalogEntry.tags,
257
+ },
258
+ },
259
+ ],
260
+ },
261
+ },
262
+ results: [
263
+ {
264
+ ruleId: "RTGL-CLI-001",
265
+ level: "error",
266
+ message: { text: message },
267
+ locations: [
268
+ {
269
+ physicalLocation: {
270
+ artifactLocation: { uri: "unknown" },
271
+ },
272
+ },
273
+ ],
274
+ },
275
+ ],
276
+ },
277
+ ],
278
+ }, null, 2);
279
+ };
280
+
281
+ const main = async () => {
282
+ let parsed = null;
283
+
284
+ try {
285
+ parsed = parseCheckArgs(process.argv.slice(2));
286
+ if (parsed.help) {
287
+ console.log(parsed.usage);
288
+ process.exitCode = 0;
289
+ return;
290
+ }
291
+
292
+ await check(parsed.options);
293
+ } catch (err) {
294
+ if (err instanceof CliArgError) {
295
+ console.error(`[Check] ${err.message}`);
296
+ console.error("");
297
+ console.error(err.usage);
298
+ process.exitCode = 1;
299
+ return;
300
+ }
301
+
302
+ const message = err instanceof Error ? err.message : String(err);
303
+ const format = parsed?.options?.format || "text";
304
+
305
+ if (format === "json") {
306
+ console.log(formatCliRuntimeErrorReport({
307
+ message,
308
+ warnAsError: Boolean(parsed?.options?.warnAsError),
309
+ }));
310
+ process.exitCode = 1;
311
+ return;
312
+ }
313
+
314
+ if (format === "sarif") {
315
+ console.log(formatCliRuntimeErrorSarif({ message }));
316
+ process.exitCode = 1;
317
+ return;
318
+ }
319
+
320
+ console.error(`[Check] ${message}`);
321
+ process.exitCode = 1;
322
+ }
323
+ };
324
+
325
+ await main();
@@ -0,0 +1,232 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { load as loadYaml } from "js-yaml";
4
+ import { analyzeProject } from "../core/analyze.js";
5
+ import { applyDiagnosticFixes } from "../diagnostics/autofix.js";
6
+ import { formatReport } from "../reporters/index.js";
7
+ import { isDirectoryPath } from "../utils/fs.js";
8
+
9
+ const readConfig = (cwd) => {
10
+ const configPath = path.resolve(cwd, "rettangoli.config.yaml");
11
+ if (!existsSync(configPath)) {
12
+ return null;
13
+ }
14
+
15
+ try {
16
+ const content = readFileSync(configPath, "utf8");
17
+ return loadYaml(content);
18
+ } catch (err) {
19
+ throw new Error(`Failed to read rettangoli.config.yaml: ${err.message}`);
20
+ }
21
+ };
22
+
23
+ const resolveDirs = ({ cwd, dirs, config }) => {
24
+ if (Array.isArray(dirs) && dirs.length > 0) {
25
+ return dirs;
26
+ }
27
+
28
+ const configDirs = config?.fe?.dirs;
29
+ if (Array.isArray(configDirs) && configDirs.length > 0) {
30
+ return configDirs;
31
+ }
32
+
33
+ return ["./src/components"];
34
+ };
35
+
36
+ const toAutofixReport = ({
37
+ autofixResult,
38
+ mode = "off",
39
+ includePatchText = false,
40
+ }) => {
41
+ if (!autofixResult) {
42
+ return undefined;
43
+ }
44
+
45
+ return {
46
+ mode,
47
+ dryRun: Boolean(autofixResult.dryRun),
48
+ patchOutput: includePatchText,
49
+ candidateCount: Number(autofixResult.candidateCount) || 0,
50
+ appliedCount: Number(autofixResult.appliedCount) || 0,
51
+ skippedCount: Number(autofixResult.skippedCount) || 0,
52
+ patches: Array.isArray(autofixResult.patches)
53
+ ? autofixResult.patches.map((patch) => ({
54
+ code: patch.code,
55
+ filePath: patch.filePath,
56
+ line: patch.line,
57
+ description: patch.description,
58
+ confidence: patch.confidence,
59
+ ...(includePatchText && typeof patch.patch === "string" ? { patch: patch.patch } : {}),
60
+ }))
61
+ : [],
62
+ };
63
+ };
64
+
65
+ export const check = async (options = {}) => {
66
+ const {
67
+ cwd = process.cwd(),
68
+ dirs = [],
69
+ format = "text",
70
+ warnAsError = false,
71
+ includeYahtml = true,
72
+ includeExpression = false,
73
+ watch = false,
74
+ watchIntervalMs = 800,
75
+ autofixMode = "off",
76
+ autofixMinConfidence = 0.9,
77
+ autofixPatch = false,
78
+ } = options;
79
+
80
+ const config = readConfig(cwd);
81
+ const resolvedDirs = resolveDirs({ cwd, dirs, config });
82
+ const missingDirs = resolvedDirs.filter((dirPath) => !isDirectoryPath(path.resolve(cwd, dirPath)));
83
+ const incrementalState = { componentCache: new Map() };
84
+
85
+ if (missingDirs.length > 0) {
86
+ throw new Error(`Component directories do not exist: ${missingDirs.join(", ")}`);
87
+ }
88
+
89
+ if (watch && autofixMode !== "off") {
90
+ throw new Error("Autofix is not supported with --watch.");
91
+ }
92
+
93
+ const runAnalysis = async () => analyzeProject({
94
+ cwd,
95
+ dirs: resolvedDirs,
96
+ workspaceRoot: cwd,
97
+ includeYahtml,
98
+ includeExpression,
99
+ incrementalState,
100
+ });
101
+
102
+ const runOnce = async () => {
103
+ const initialResult = await runAnalysis();
104
+ let result = initialResult;
105
+
106
+ let autofixResult = null;
107
+ if (autofixMode !== "off") {
108
+ autofixResult = applyDiagnosticFixes({
109
+ diagnostics: initialResult.diagnostics,
110
+ dryRun: autofixMode !== "apply",
111
+ minConfidence: autofixMinConfidence,
112
+ includePatchText: autofixPatch,
113
+ });
114
+
115
+ if (autofixMode === "apply" && autofixResult.appliedCount > 0) {
116
+ result = await runAnalysis();
117
+ }
118
+
119
+ result = {
120
+ ...result,
121
+ autofix: toAutofixReport({
122
+ autofixResult,
123
+ mode: autofixMode,
124
+ includePatchText: autofixPatch,
125
+ }),
126
+ };
127
+ }
128
+
129
+ const report = formatReport({
130
+ format: (format === "json" || format === "sarif") ? format : "text",
131
+ result,
132
+ warnAsError,
133
+ });
134
+
135
+ const hasErrors = result.summary.bySeverity.error > 0;
136
+ const hasWarnAsError = warnAsError && result.summary.bySeverity.warn > 0;
137
+ process.exitCode = hasErrors || hasWarnAsError ? 1 : 0;
138
+
139
+ return {
140
+ result,
141
+ report,
142
+ hasErrors,
143
+ hasWarnAsError,
144
+ };
145
+ };
146
+
147
+ if (!watch) {
148
+ const {
149
+ result,
150
+ report,
151
+ hasErrors,
152
+ hasWarnAsError,
153
+ } = await runOnce();
154
+ if (format === "json" || format === "sarif") {
155
+ console.log(report);
156
+ } else if (hasErrors || hasWarnAsError) {
157
+ console.error(report);
158
+ } else {
159
+ console.log(report);
160
+ }
161
+ return result;
162
+ }
163
+
164
+ let lastSignature = null;
165
+ let running = false;
166
+ let stopped = false;
167
+ let lastResult = null;
168
+ const interval = Math.max(200, Number.isFinite(watchIntervalMs) ? Number(watchIntervalMs) : 800);
169
+
170
+ const runWatchIteration = async () => {
171
+ if (running || stopped) {
172
+ return;
173
+ }
174
+ running = true;
175
+ try {
176
+ const { result, report } = await runOnce();
177
+ const signature = JSON.stringify({
178
+ summary: result.summary,
179
+ diagnostics: result.diagnostics,
180
+ });
181
+ if (signature !== lastSignature) {
182
+ if (lastSignature !== null && format === "text") {
183
+ console.log("\n[Watch] Change detected.\n");
184
+ }
185
+ if (format === "json" || format === "sarif") {
186
+ console.log(report);
187
+ } else {
188
+ const hasErrors = result.summary.bySeverity.error > 0;
189
+ const hasWarnAsError = warnAsError && result.summary.bySeverity.warn > 0;
190
+ if (hasErrors || hasWarnAsError) {
191
+ console.error(report);
192
+ } else {
193
+ console.log(report);
194
+ }
195
+ }
196
+ lastSignature = signature;
197
+ }
198
+ lastResult = result;
199
+ } catch (err) {
200
+ const message = err instanceof Error ? err.message : String(err);
201
+ if (format === "text") {
202
+ console.error(`[Watch] ${message}`);
203
+ }
204
+ } finally {
205
+ running = false;
206
+ }
207
+ };
208
+
209
+ await runWatchIteration();
210
+ const timer = setInterval(runWatchIteration, interval);
211
+
212
+ const stop = () => {
213
+ stopped = true;
214
+ clearInterval(timer);
215
+ };
216
+
217
+ process.on("SIGINT", stop);
218
+ process.on("SIGTERM", stop);
219
+
220
+ await new Promise((resolve) => {
221
+ const checkStopped = setInterval(() => {
222
+ if (stopped) {
223
+ clearInterval(checkStopped);
224
+ resolve();
225
+ }
226
+ }, 100);
227
+ });
228
+
229
+ return lastResult;
230
+ };
231
+
232
+ export default check;
@@ -0,0 +1 @@
1
+ export { default as check } from "./check.js";