@chrisdudek/yg 5.0.3 → 5.1.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/bin.js +2115 -762
- package/dist/structure.d.ts +30 -11
- package/dist/structure.js +298 -273
- package/package.json +2 -3
- package/graph-schemas/yg-architecture.yaml +0 -62
- package/graph-schemas/yg-aspect.yaml +0 -194
- package/graph-schemas/yg-config.yaml +0 -53
- package/graph-schemas/yg-flow.yaml +0 -25
- package/graph-schemas/yg-node.yaml +0 -50
package/dist/structure.js
CHANGED
|
@@ -1,31 +1,6 @@
|
|
|
1
|
-
// src/structure/runner.ts
|
|
2
|
-
import * as fs4 from "fs";
|
|
3
|
-
import * as path8 from "path";
|
|
4
|
-
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
5
|
-
|
|
6
|
-
// src/ast/loader-hook.ts
|
|
7
|
-
import { register } from "module";
|
|
8
|
-
import { pathToFileURL } from "url";
|
|
9
|
-
import path from "path";
|
|
10
|
-
import { fileURLToPath } from "url";
|
|
11
|
-
import { existsSync } from "fs";
|
|
12
|
-
var __filename = fileURLToPath(import.meta.url);
|
|
13
|
-
var __dirname = path.dirname(__filename);
|
|
14
|
-
var registered = false;
|
|
15
|
-
function ensureLoaderRegistered() {
|
|
16
|
-
if (registered) return;
|
|
17
|
-
let implPath = path.resolve(__dirname, "./loader-hook-impl.js");
|
|
18
|
-
if (!existsSync(implPath)) {
|
|
19
|
-
const pkgRoot = path.resolve(__dirname, "../../");
|
|
20
|
-
implPath = path.resolve(pkgRoot, "dist/loader-hook-impl.js");
|
|
21
|
-
}
|
|
22
|
-
register(pathToFileURL(implPath));
|
|
23
|
-
registered = true;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
1
|
// src/structure/ctx-fs.ts
|
|
27
2
|
import * as fs from "fs";
|
|
28
|
-
import * as
|
|
3
|
+
import * as path from "path";
|
|
29
4
|
|
|
30
5
|
// src/utils/mapping-path.ts
|
|
31
6
|
import { minimatch } from "minimatch";
|
|
@@ -81,7 +56,7 @@ function assertRealpathContained(abs, projectRoot, rel) {
|
|
|
81
56
|
}
|
|
82
57
|
let probe = abs;
|
|
83
58
|
while (!fs.existsSync(probe)) {
|
|
84
|
-
const parent =
|
|
59
|
+
const parent = path.dirname(probe);
|
|
85
60
|
if (parent === probe) return;
|
|
86
61
|
probe = parent;
|
|
87
62
|
}
|
|
@@ -91,15 +66,15 @@ function assertRealpathContained(abs, projectRoot, rel) {
|
|
|
91
66
|
} catch {
|
|
92
67
|
return;
|
|
93
68
|
}
|
|
94
|
-
const relReal = toPosix(
|
|
95
|
-
if (relReal === ".." || relReal.startsWith("../") ||
|
|
69
|
+
const relReal = toPosix(path.relative(realRoot, realProbe));
|
|
70
|
+
if (relReal === ".." || relReal.startsWith("../") || path.isAbsolute(relReal)) {
|
|
96
71
|
throw new UndeclaredFsReadError(rel);
|
|
97
72
|
}
|
|
98
73
|
}
|
|
99
74
|
function resolveAllowedReadPath(raw, allowedSet, projectRoot) {
|
|
100
|
-
const abs =
|
|
101
|
-
const rel = toPosix(
|
|
102
|
-
if (rel === "" || rel.startsWith("..") ||
|
|
75
|
+
const abs = path.resolve(projectRoot, normalizeMappingPath(raw));
|
|
76
|
+
const rel = toPosix(path.relative(projectRoot, abs));
|
|
77
|
+
if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
103
78
|
throw new UndeclaredFsReadError(normalizeMappingPath(raw));
|
|
104
79
|
}
|
|
105
80
|
if (!isAllowed(rel, allowedSet)) throw new UndeclaredFsReadError(rel);
|
|
@@ -119,7 +94,7 @@ function createCtxFs(params) {
|
|
|
119
94
|
return {
|
|
120
95
|
exists(raw) {
|
|
121
96
|
const p = assertAllowed(raw);
|
|
122
|
-
const abs =
|
|
97
|
+
const abs = path.resolve(projectRoot, p);
|
|
123
98
|
let result;
|
|
124
99
|
try {
|
|
125
100
|
const stat2 = fs.statSync(abs);
|
|
@@ -134,7 +109,7 @@ function createCtxFs(params) {
|
|
|
134
109
|
},
|
|
135
110
|
read(raw) {
|
|
136
111
|
const p = assertAllowed(raw);
|
|
137
|
-
const abs =
|
|
112
|
+
const abs = path.resolve(projectRoot, p);
|
|
138
113
|
let bytes;
|
|
139
114
|
try {
|
|
140
115
|
bytes = fs.readFileSync(abs);
|
|
@@ -149,7 +124,7 @@ function createCtxFs(params) {
|
|
|
149
124
|
},
|
|
150
125
|
list(raw) {
|
|
151
126
|
const p = assertAllowed(raw);
|
|
152
|
-
const abs =
|
|
127
|
+
const abs = path.resolve(projectRoot, p);
|
|
153
128
|
let dirents;
|
|
154
129
|
try {
|
|
155
130
|
dirents = fs.readdirSync(abs, { withFileTypes: true });
|
|
@@ -171,7 +146,7 @@ function createCtxFs(params) {
|
|
|
171
146
|
|
|
172
147
|
// src/structure/ctx-graph.ts
|
|
173
148
|
import * as fs2 from "fs";
|
|
174
|
-
import * as
|
|
149
|
+
import * as path2 from "path";
|
|
175
150
|
|
|
176
151
|
// src/structure/expand-mapping-sync.ts
|
|
177
152
|
function isPathInMapping(candidate, mapping) {
|
|
@@ -225,7 +200,7 @@ function createCtxGraph(params) {
|
|
|
225
200
|
}
|
|
226
201
|
function recordGraphNode(m) {
|
|
227
202
|
if (!recorder) return;
|
|
228
|
-
const ygNodePath =
|
|
203
|
+
const ygNodePath = path2.join(projectRoot, ".yggdrasil", "model", m.path, "yg-node.yaml");
|
|
229
204
|
let yamlBytes;
|
|
230
205
|
try {
|
|
231
206
|
yamlBytes = fs2.readFileSync(ygNodePath);
|
|
@@ -244,7 +219,7 @@ function createCtxGraph(params) {
|
|
|
244
219
|
const candidatePaths = preExpanded ?? (m.meta.mapping ?? []).map(normalizeMappingPath);
|
|
245
220
|
for (const p of candidatePaths) {
|
|
246
221
|
if (!p) continue;
|
|
247
|
-
const abs =
|
|
222
|
+
const abs = path2.resolve(projectRoot, p);
|
|
248
223
|
try {
|
|
249
224
|
const stat2 = fs2.statSync(abs);
|
|
250
225
|
if (stat2.isFile()) {
|
|
@@ -342,16 +317,16 @@ function createCtxGraph(params) {
|
|
|
342
317
|
|
|
343
318
|
// src/structure/ctx-parsers.ts
|
|
344
319
|
import * as fs3 from "fs";
|
|
345
|
-
import * as
|
|
320
|
+
import * as path4 from "path";
|
|
346
321
|
import { extname } from "path";
|
|
347
322
|
import { parse as parseYaml } from "yaml";
|
|
348
323
|
import { parse as parseTomlSmol } from "smol-toml";
|
|
349
324
|
|
|
350
325
|
// src/ast/parser.ts
|
|
351
326
|
import { Parser, Language } from "web-tree-sitter";
|
|
352
|
-
import
|
|
353
|
-
import { fileURLToPath
|
|
354
|
-
import { existsSync as
|
|
327
|
+
import path3 from "path";
|
|
328
|
+
import { fileURLToPath } from "url";
|
|
329
|
+
import { existsSync as existsSync2 } from "fs";
|
|
355
330
|
import { createRequire } from "module";
|
|
356
331
|
|
|
357
332
|
// src/core/graph/language-registry.ts
|
|
@@ -566,11 +541,11 @@ function getGrammarForExtension(ext) {
|
|
|
566
541
|
|
|
567
542
|
// src/ast/parser.ts
|
|
568
543
|
var _require = createRequire(import.meta.url);
|
|
569
|
-
var
|
|
570
|
-
var
|
|
544
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
545
|
+
var __dirname = path3.dirname(__filename);
|
|
571
546
|
var GRAMMAR_DIRS = [
|
|
572
|
-
|
|
573
|
-
|
|
547
|
+
path3.resolve(__dirname, "grammars"),
|
|
548
|
+
path3.resolve(__dirname, "..", "grammars")
|
|
574
549
|
];
|
|
575
550
|
var initPromise = null;
|
|
576
551
|
var langCache = /* @__PURE__ */ new Map();
|
|
@@ -585,13 +560,13 @@ function init() {
|
|
|
585
560
|
}
|
|
586
561
|
function resolveWasm(filename, pkg) {
|
|
587
562
|
for (const dir of GRAMMAR_DIRS) {
|
|
588
|
-
const p =
|
|
589
|
-
if (
|
|
563
|
+
const p = path3.join(dir, filename);
|
|
564
|
+
if (existsSync2(p)) return p;
|
|
590
565
|
}
|
|
591
566
|
try {
|
|
592
|
-
const pkgDir =
|
|
593
|
-
for (const candidate of [
|
|
594
|
-
if (
|
|
567
|
+
const pkgDir = path3.dirname(_require.resolve(`${pkg}/package.json`));
|
|
568
|
+
for (const candidate of [path3.join(pkgDir, filename), path3.join(pkgDir, "bindings/node", filename)]) {
|
|
569
|
+
if (existsSync2(candidate)) return candidate;
|
|
595
570
|
}
|
|
596
571
|
} catch {
|
|
597
572
|
}
|
|
@@ -619,7 +594,7 @@ async function getParser(extension) {
|
|
|
619
594
|
return parser;
|
|
620
595
|
}
|
|
621
596
|
async function parseFile(filePath, content) {
|
|
622
|
-
const ext =
|
|
597
|
+
const ext = path3.extname(filePath);
|
|
623
598
|
const parser = await getParser(ext);
|
|
624
599
|
const tree = parser.parse(content);
|
|
625
600
|
if (tree === null) {
|
|
@@ -647,7 +622,7 @@ function createCtxParsers(params) {
|
|
|
647
622
|
return input;
|
|
648
623
|
}
|
|
649
624
|
const p = resolveAllowedReadPath(input, allowedSet, projectRoot);
|
|
650
|
-
const abs =
|
|
625
|
+
const abs = path4.resolve(projectRoot, p);
|
|
651
626
|
let bytes;
|
|
652
627
|
try {
|
|
653
628
|
bytes = fs3.readFileSync(abs);
|
|
@@ -703,6 +678,226 @@ function enrichFilesWithAst(files, astCache) {
|
|
|
703
678
|
});
|
|
704
679
|
}
|
|
705
680
|
|
|
681
|
+
// src/ast/suppress.ts
|
|
682
|
+
import { extname as extname3 } from "path";
|
|
683
|
+
|
|
684
|
+
// src/ast/find-comments.ts
|
|
685
|
+
import { extname as extname2 } from "path";
|
|
686
|
+
function findComments(target) {
|
|
687
|
+
const hasAst = "ast" in target;
|
|
688
|
+
const hasRootNode = "rootNode" in target;
|
|
689
|
+
if (hasAst && hasRootNode) {
|
|
690
|
+
throw new Error("AST_FINDCOMMENTS_AMBIGUOUS_TARGET: pass either ast or rootNode, not both");
|
|
691
|
+
}
|
|
692
|
+
let language = "language" in target ? target.language : void 0;
|
|
693
|
+
if (language === void 0 && "path" in target) {
|
|
694
|
+
language = getLanguageForExtension(extname2(target.path)) ?? void 0;
|
|
695
|
+
}
|
|
696
|
+
if (language === void 0) {
|
|
697
|
+
throw new Error(
|
|
698
|
+
"AST_FINDCOMMENTS_NO_LANGUAGE: pass a SourceFile whose path has a known extension, or an explicit { language }"
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
const def = LANGUAGES[language];
|
|
702
|
+
if (def === void 0) {
|
|
703
|
+
throw new Error(`AST_FINDCOMMENTS_UNKNOWN_LANGUAGE: '${language}' not in registry`);
|
|
704
|
+
}
|
|
705
|
+
const root = hasAst ? target.ast.rootNode : target.rootNode;
|
|
706
|
+
const out = [];
|
|
707
|
+
for (const type of def.commentTypes) {
|
|
708
|
+
out.push(...root.descendantsOfType(type));
|
|
709
|
+
}
|
|
710
|
+
return out;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// src/ast/suppress.ts
|
|
714
|
+
var SuppressMarkerError = class extends Error {
|
|
715
|
+
constructor(message, file, line) {
|
|
716
|
+
super(message);
|
|
717
|
+
this.file = file;
|
|
718
|
+
this.line = line;
|
|
719
|
+
this.name = "SuppressMarkerError";
|
|
720
|
+
}
|
|
721
|
+
file;
|
|
722
|
+
line;
|
|
723
|
+
code = "SUPPRESS_MARKER_MISSING_REASON";
|
|
724
|
+
};
|
|
725
|
+
var RE_SINGLE = /\byg-suppress\(\s*([^)]+?)\s*\)\s*(.+)?$/m;
|
|
726
|
+
var RE_DISABLE = /\byg-suppress-disable\(\s*([^)]+?)\s*\)\s*(.+)?$/m;
|
|
727
|
+
var RE_ENABLE = /\byg-suppress-enable\(\s*([^)]+?)\s*\)/;
|
|
728
|
+
function commentBody(text) {
|
|
729
|
+
if (text.startsWith("//")) return text.slice(2).trim();
|
|
730
|
+
if (text.startsWith("/*")) return text.replace(/^\/\*+/, "").replace(/\*+\/$/, "").trim();
|
|
731
|
+
return text.trim();
|
|
732
|
+
}
|
|
733
|
+
function splitAspectList(raw) {
|
|
734
|
+
return raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
735
|
+
}
|
|
736
|
+
function makeMarker(kind, match, line, file) {
|
|
737
|
+
const aspectIds = splitAspectList(match[1]);
|
|
738
|
+
const reason = (match[2] ?? "").trim();
|
|
739
|
+
if (reason === "") {
|
|
740
|
+
throw new SuppressMarkerError(
|
|
741
|
+
`yg-suppress${kind === "disable" ? "-disable" : ""}(${match[1]}) missing reason at ${file}:${line}`,
|
|
742
|
+
file,
|
|
743
|
+
line
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
return { kind, aspectIds, reason, line };
|
|
747
|
+
}
|
|
748
|
+
function parseMarker(commentText, line, file) {
|
|
749
|
+
const body = commentBody(commentText);
|
|
750
|
+
let m = body.match(RE_DISABLE);
|
|
751
|
+
if (m) return makeMarker("disable", m, line, file);
|
|
752
|
+
m = body.match(RE_ENABLE);
|
|
753
|
+
if (m) {
|
|
754
|
+
return { kind: "enable", aspectIds: splitAspectList(m[1]), reason: "", line };
|
|
755
|
+
}
|
|
756
|
+
m = body.match(RE_SINGLE);
|
|
757
|
+
if (m) return makeMarker("single", m, line, file);
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
function collectSuppressions(tree, file, totalLines, content) {
|
|
761
|
+
const hasGrammar = getLanguageForExtension(extname3(file)) !== null;
|
|
762
|
+
const markers = [];
|
|
763
|
+
if (hasGrammar && tree) {
|
|
764
|
+
const comments = findComments({ path: file, ast: tree });
|
|
765
|
+
for (const c of comments) {
|
|
766
|
+
const m = parseMarker(c.text, c.startPosition.row + 1, file);
|
|
767
|
+
if (m) markers.push(m);
|
|
768
|
+
}
|
|
769
|
+
} else if (content !== void 0) {
|
|
770
|
+
const lines = content.split("\n");
|
|
771
|
+
for (let i = 0; i < lines.length; i++) {
|
|
772
|
+
const m = parseMarker(lines[i], i + 1, file);
|
|
773
|
+
if (m) markers.push(m);
|
|
774
|
+
}
|
|
775
|
+
} else {
|
|
776
|
+
return [];
|
|
777
|
+
}
|
|
778
|
+
markers.sort((a, b) => a.line - b.line);
|
|
779
|
+
const ranges = [];
|
|
780
|
+
const openSpecific = /* @__PURE__ */ new Map();
|
|
781
|
+
let openWildcard = null;
|
|
782
|
+
for (const m of markers) {
|
|
783
|
+
if (m.kind === "single") {
|
|
784
|
+
const isWildcard = m.aspectIds.includes("*");
|
|
785
|
+
ranges.push({ aspectIds: new Set(m.aspectIds), startLine: m.line + 1, endLine: m.line + 1, isWildcard });
|
|
786
|
+
} else if (m.kind === "disable") {
|
|
787
|
+
for (const id of m.aspectIds) {
|
|
788
|
+
if (id === "*") {
|
|
789
|
+
if (openWildcard === null) openWildcard = m.line + 1;
|
|
790
|
+
} else {
|
|
791
|
+
if (!openSpecific.has(id)) openSpecific.set(id, m.line + 1);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
} else {
|
|
795
|
+
for (const id of m.aspectIds) {
|
|
796
|
+
if (id === "*") {
|
|
797
|
+
if (openWildcard !== null) {
|
|
798
|
+
ranges.push({ aspectIds: /* @__PURE__ */ new Set(["*"]), startLine: openWildcard, endLine: m.line - 1, isWildcard: true });
|
|
799
|
+
openWildcard = null;
|
|
800
|
+
}
|
|
801
|
+
} else {
|
|
802
|
+
const start = openSpecific.get(id);
|
|
803
|
+
if (start !== void 0) {
|
|
804
|
+
ranges.push({ aspectIds: /* @__PURE__ */ new Set([id]), startLine: start, endLine: m.line - 1, isWildcard: false });
|
|
805
|
+
openSpecific.delete(id);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
if (openWildcard !== null) ranges.push({ aspectIds: /* @__PURE__ */ new Set(["*"]), startLine: openWildcard, endLine: totalLines, isWildcard: true });
|
|
812
|
+
for (const [id, start] of openSpecific) {
|
|
813
|
+
ranges.push({ aspectIds: /* @__PURE__ */ new Set([id]), startLine: start, endLine: totalLines, isWildcard: false });
|
|
814
|
+
}
|
|
815
|
+
return ranges;
|
|
816
|
+
}
|
|
817
|
+
function isLineSuppressed(ranges, aspectId, line) {
|
|
818
|
+
return ranges.some((r) => {
|
|
819
|
+
if (line < r.startLine || line > r.endLine) return false;
|
|
820
|
+
return r.isWildcard || r.aspectIds.has(aspectId);
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// src/utils/validate-check-module.ts
|
|
825
|
+
function validateCheckModuleExport(mod, opts) {
|
|
826
|
+
const { codePrefix, runnerLabel } = opts;
|
|
827
|
+
if (mod.check === void 0) {
|
|
828
|
+
const defaultExport = mod.default;
|
|
829
|
+
if (typeof defaultExport === "function" && defaultExport.name === "check") {
|
|
830
|
+
return {
|
|
831
|
+
ok: false,
|
|
832
|
+
code: `${codePrefix}_CHECK_DEFAULT_EXPORT`,
|
|
833
|
+
message: {
|
|
834
|
+
what: `check.mjs exports 'check' as default, but a NAMED export is required (${runnerLabel}).`,
|
|
835
|
+
why: `The runner imports the named export. A default export is invisible to it.`,
|
|
836
|
+
next: `Change 'export default function check(...)' to 'export function check(...)'.`
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
return {
|
|
841
|
+
ok: false,
|
|
842
|
+
code: `${codePrefix}_CHECK_NOT_EXPORTED`,
|
|
843
|
+
message: {
|
|
844
|
+
what: `check.mjs does not export a function named 'check' (${runnerLabel}).`,
|
|
845
|
+
why: `The runner expects 'export function check(ctx) { ... }'.`,
|
|
846
|
+
next: `Add a named export 'check' in check.mjs.`
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
if (typeof mod.check !== "function") {
|
|
851
|
+
return {
|
|
852
|
+
ok: false,
|
|
853
|
+
code: `${codePrefix}_CHECK_NOT_FUNCTION`,
|
|
854
|
+
message: {
|
|
855
|
+
what: `'check' is exported but is not a function (got ${typeof mod.check}).`,
|
|
856
|
+
why: `The runner calls check(ctx).`,
|
|
857
|
+
next: `Re-export check as a function.`
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
const checkFn = mod.check;
|
|
862
|
+
if (checkFn.length !== 1) {
|
|
863
|
+
return {
|
|
864
|
+
ok: false,
|
|
865
|
+
code: `${codePrefix}_CHECK_WRONG_ARITY`,
|
|
866
|
+
message: {
|
|
867
|
+
what: `'check' must accept exactly 1 parameter (ctx); declared arity is ${checkFn.length}.`,
|
|
868
|
+
why: `The runner invokes check(ctx).`,
|
|
869
|
+
next: `Change the signature to function check(ctx).`
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
return { ok: true };
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// src/structure/hook-loader.ts
|
|
877
|
+
import * as fs4 from "fs";
|
|
878
|
+
import * as path8 from "path";
|
|
879
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
880
|
+
|
|
881
|
+
// src/ast/loader-hook.ts
|
|
882
|
+
import { register } from "module";
|
|
883
|
+
import { pathToFileURL } from "url";
|
|
884
|
+
import path5 from "path";
|
|
885
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
886
|
+
import { existsSync as existsSync3 } from "fs";
|
|
887
|
+
var __filename2 = fileURLToPath2(import.meta.url);
|
|
888
|
+
var __dirname2 = path5.dirname(__filename2);
|
|
889
|
+
var registered = false;
|
|
890
|
+
function ensureLoaderRegistered() {
|
|
891
|
+
if (registered) return;
|
|
892
|
+
let implPath = path5.resolve(__dirname2, "./loader-hook-impl.js");
|
|
893
|
+
if (!existsSync3(implPath)) {
|
|
894
|
+
const pkgRoot = path5.resolve(__dirname2, "../../");
|
|
895
|
+
implPath = path5.resolve(pkgRoot, "dist/loader-hook-impl.js");
|
|
896
|
+
}
|
|
897
|
+
register(pathToFileURL(implPath));
|
|
898
|
+
registered = true;
|
|
899
|
+
}
|
|
900
|
+
|
|
706
901
|
// src/structure/allowed-reads.ts
|
|
707
902
|
function collectAllowedReadsForAspect(nodePath, graph) {
|
|
708
903
|
const allowed = /* @__PURE__ */ new Set();
|
|
@@ -922,201 +1117,6 @@ async function expandMappingPaths(projectRoot, mappingPaths) {
|
|
|
922
1117
|
return result;
|
|
923
1118
|
}
|
|
924
1119
|
|
|
925
|
-
// src/ast/suppress.ts
|
|
926
|
-
import { extname as extname3 } from "path";
|
|
927
|
-
|
|
928
|
-
// src/ast/find-comments.ts
|
|
929
|
-
import { extname as extname2 } from "path";
|
|
930
|
-
function findComments(target) {
|
|
931
|
-
const hasAst = "ast" in target;
|
|
932
|
-
const hasRootNode = "rootNode" in target;
|
|
933
|
-
if (hasAst && hasRootNode) {
|
|
934
|
-
throw new Error("AST_FINDCOMMENTS_AMBIGUOUS_TARGET: pass either ast or rootNode, not both");
|
|
935
|
-
}
|
|
936
|
-
let language = "language" in target ? target.language : void 0;
|
|
937
|
-
if (language === void 0 && "path" in target) {
|
|
938
|
-
language = getLanguageForExtension(extname2(target.path)) ?? void 0;
|
|
939
|
-
}
|
|
940
|
-
if (language === void 0) {
|
|
941
|
-
throw new Error(
|
|
942
|
-
"AST_FINDCOMMENTS_NO_LANGUAGE: pass a SourceFile whose path has a known extension, or an explicit { language }"
|
|
943
|
-
);
|
|
944
|
-
}
|
|
945
|
-
const def = LANGUAGES[language];
|
|
946
|
-
if (def === void 0) {
|
|
947
|
-
throw new Error(`AST_FINDCOMMENTS_UNKNOWN_LANGUAGE: '${language}' not in registry`);
|
|
948
|
-
}
|
|
949
|
-
const root = hasAst ? target.ast.rootNode : target.rootNode;
|
|
950
|
-
const out = [];
|
|
951
|
-
for (const type of def.commentTypes) {
|
|
952
|
-
out.push(...root.descendantsOfType(type));
|
|
953
|
-
}
|
|
954
|
-
return out;
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
// src/ast/suppress.ts
|
|
958
|
-
var SuppressMarkerError = class extends Error {
|
|
959
|
-
constructor(message, file, line) {
|
|
960
|
-
super(message);
|
|
961
|
-
this.file = file;
|
|
962
|
-
this.line = line;
|
|
963
|
-
this.name = "SuppressMarkerError";
|
|
964
|
-
}
|
|
965
|
-
file;
|
|
966
|
-
line;
|
|
967
|
-
code = "SUPPRESS_MARKER_MISSING_REASON";
|
|
968
|
-
};
|
|
969
|
-
var RE_SINGLE = /\byg-suppress\(\s*([^)]+?)\s*\)\s*(.+)?$/m;
|
|
970
|
-
var RE_DISABLE = /\byg-suppress-disable\(\s*([^)]+?)\s*\)\s*(.+)?$/m;
|
|
971
|
-
var RE_ENABLE = /\byg-suppress-enable\(\s*([^)]+?)\s*\)/;
|
|
972
|
-
function commentBody(text) {
|
|
973
|
-
if (text.startsWith("//")) return text.slice(2).trim();
|
|
974
|
-
if (text.startsWith("/*")) return text.replace(/^\/\*+/, "").replace(/\*+\/$/, "").trim();
|
|
975
|
-
return text.trim();
|
|
976
|
-
}
|
|
977
|
-
function splitAspectList(raw) {
|
|
978
|
-
return raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
979
|
-
}
|
|
980
|
-
function makeMarker(kind, match, line, file) {
|
|
981
|
-
const aspectIds = splitAspectList(match[1]);
|
|
982
|
-
const reason = (match[2] ?? "").trim();
|
|
983
|
-
if (reason === "") {
|
|
984
|
-
throw new SuppressMarkerError(
|
|
985
|
-
`yg-suppress${kind === "disable" ? "-disable" : ""}(${match[1]}) missing reason at ${file}:${line}`,
|
|
986
|
-
file,
|
|
987
|
-
line
|
|
988
|
-
);
|
|
989
|
-
}
|
|
990
|
-
return { kind, aspectIds, reason, line };
|
|
991
|
-
}
|
|
992
|
-
function parseMarker(commentText, line, file) {
|
|
993
|
-
const body = commentBody(commentText);
|
|
994
|
-
let m = body.match(RE_DISABLE);
|
|
995
|
-
if (m) return makeMarker("disable", m, line, file);
|
|
996
|
-
m = body.match(RE_ENABLE);
|
|
997
|
-
if (m) {
|
|
998
|
-
return { kind: "enable", aspectIds: splitAspectList(m[1]), reason: "", line };
|
|
999
|
-
}
|
|
1000
|
-
m = body.match(RE_SINGLE);
|
|
1001
|
-
if (m) return makeMarker("single", m, line, file);
|
|
1002
|
-
return null;
|
|
1003
|
-
}
|
|
1004
|
-
function collectSuppressions(tree, file, totalLines, content) {
|
|
1005
|
-
const hasGrammar = getLanguageForExtension(extname3(file)) !== null;
|
|
1006
|
-
const markers = [];
|
|
1007
|
-
if (hasGrammar && tree) {
|
|
1008
|
-
const comments = findComments({ path: file, ast: tree });
|
|
1009
|
-
for (const c of comments) {
|
|
1010
|
-
const m = parseMarker(c.text, c.startPosition.row + 1, file);
|
|
1011
|
-
if (m) markers.push(m);
|
|
1012
|
-
}
|
|
1013
|
-
} else if (content !== void 0) {
|
|
1014
|
-
const lines = content.split("\n");
|
|
1015
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1016
|
-
const m = parseMarker(lines[i], i + 1, file);
|
|
1017
|
-
if (m) markers.push(m);
|
|
1018
|
-
}
|
|
1019
|
-
} else {
|
|
1020
|
-
return [];
|
|
1021
|
-
}
|
|
1022
|
-
markers.sort((a, b) => a.line - b.line);
|
|
1023
|
-
const ranges = [];
|
|
1024
|
-
const openSpecific = /* @__PURE__ */ new Map();
|
|
1025
|
-
let openWildcard = null;
|
|
1026
|
-
for (const m of markers) {
|
|
1027
|
-
if (m.kind === "single") {
|
|
1028
|
-
const isWildcard = m.aspectIds.includes("*");
|
|
1029
|
-
ranges.push({ aspectIds: new Set(m.aspectIds), startLine: m.line + 1, endLine: m.line + 1, isWildcard });
|
|
1030
|
-
} else if (m.kind === "disable") {
|
|
1031
|
-
for (const id of m.aspectIds) {
|
|
1032
|
-
if (id === "*") {
|
|
1033
|
-
if (openWildcard === null) openWildcard = m.line + 1;
|
|
1034
|
-
} else {
|
|
1035
|
-
if (!openSpecific.has(id)) openSpecific.set(id, m.line + 1);
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
} else {
|
|
1039
|
-
for (const id of m.aspectIds) {
|
|
1040
|
-
if (id === "*") {
|
|
1041
|
-
if (openWildcard !== null) {
|
|
1042
|
-
ranges.push({ aspectIds: /* @__PURE__ */ new Set(["*"]), startLine: openWildcard, endLine: m.line - 1, isWildcard: true });
|
|
1043
|
-
openWildcard = null;
|
|
1044
|
-
}
|
|
1045
|
-
} else {
|
|
1046
|
-
const start = openSpecific.get(id);
|
|
1047
|
-
if (start !== void 0) {
|
|
1048
|
-
ranges.push({ aspectIds: /* @__PURE__ */ new Set([id]), startLine: start, endLine: m.line - 1, isWildcard: false });
|
|
1049
|
-
openSpecific.delete(id);
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
if (openWildcard !== null) ranges.push({ aspectIds: /* @__PURE__ */ new Set(["*"]), startLine: openWildcard, endLine: totalLines, isWildcard: true });
|
|
1056
|
-
for (const [id, start] of openSpecific) {
|
|
1057
|
-
ranges.push({ aspectIds: /* @__PURE__ */ new Set([id]), startLine: start, endLine: totalLines, isWildcard: false });
|
|
1058
|
-
}
|
|
1059
|
-
return ranges;
|
|
1060
|
-
}
|
|
1061
|
-
function isLineSuppressed(ranges, aspectId, line) {
|
|
1062
|
-
return ranges.some((r) => {
|
|
1063
|
-
if (line < r.startLine || line > r.endLine) return false;
|
|
1064
|
-
return r.isWildcard || r.aspectIds.has(aspectId);
|
|
1065
|
-
});
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
// src/utils/validate-check-module.ts
|
|
1069
|
-
function validateCheckModuleExport(mod, opts) {
|
|
1070
|
-
const { codePrefix, runnerLabel } = opts;
|
|
1071
|
-
if (mod.check === void 0) {
|
|
1072
|
-
const defaultExport = mod.default;
|
|
1073
|
-
if (typeof defaultExport === "function" && defaultExport.name === "check") {
|
|
1074
|
-
return {
|
|
1075
|
-
ok: false,
|
|
1076
|
-
code: `${codePrefix}_CHECK_DEFAULT_EXPORT`,
|
|
1077
|
-
message: {
|
|
1078
|
-
what: `check.mjs exports 'check' as default, but a NAMED export is required (${runnerLabel}).`,
|
|
1079
|
-
why: `The runner imports the named export. A default export is invisible to it.`,
|
|
1080
|
-
next: `Change 'export default function check(...)' to 'export function check(...)'.`
|
|
1081
|
-
}
|
|
1082
|
-
};
|
|
1083
|
-
}
|
|
1084
|
-
return {
|
|
1085
|
-
ok: false,
|
|
1086
|
-
code: `${codePrefix}_CHECK_NOT_EXPORTED`,
|
|
1087
|
-
message: {
|
|
1088
|
-
what: `check.mjs does not export a function named 'check' (${runnerLabel}).`,
|
|
1089
|
-
why: `The runner expects 'export function check(ctx) { ... }'.`,
|
|
1090
|
-
next: `Add a named export 'check' in check.mjs.`
|
|
1091
|
-
}
|
|
1092
|
-
};
|
|
1093
|
-
}
|
|
1094
|
-
if (typeof mod.check !== "function") {
|
|
1095
|
-
return {
|
|
1096
|
-
ok: false,
|
|
1097
|
-
code: `${codePrefix}_CHECK_NOT_FUNCTION`,
|
|
1098
|
-
message: {
|
|
1099
|
-
what: `'check' is exported but is not a function (got ${typeof mod.check}).`,
|
|
1100
|
-
why: `The runner calls check(ctx).`,
|
|
1101
|
-
next: `Re-export check as a function.`
|
|
1102
|
-
}
|
|
1103
|
-
};
|
|
1104
|
-
}
|
|
1105
|
-
const checkFn = mod.check;
|
|
1106
|
-
if (checkFn.length !== 1) {
|
|
1107
|
-
return {
|
|
1108
|
-
ok: false,
|
|
1109
|
-
code: `${codePrefix}_CHECK_WRONG_ARITY`,
|
|
1110
|
-
message: {
|
|
1111
|
-
what: `'check' must accept exactly 1 parameter (ctx); declared arity is ${checkFn.length}.`,
|
|
1112
|
-
why: `The runner invokes check(ctx).`,
|
|
1113
|
-
next: `Change the signature to function check(ctx).`
|
|
1114
|
-
}
|
|
1115
|
-
};
|
|
1116
|
-
}
|
|
1117
|
-
return { ok: true };
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
1120
|
// src/utils/binary-extensions.ts
|
|
1121
1121
|
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1122
1122
|
".gif",
|
|
@@ -1270,7 +1270,7 @@ var ObservationRecorder = class {
|
|
|
1270
1270
|
}
|
|
1271
1271
|
};
|
|
1272
1272
|
|
|
1273
|
-
// src/structure/
|
|
1273
|
+
// src/structure/hook-loader.ts
|
|
1274
1274
|
var StructureRunnerError = class extends Error {
|
|
1275
1275
|
constructor(code, data) {
|
|
1276
1276
|
super(`${code}: ${data.what}
|
|
@@ -1332,11 +1332,24 @@ async function enumerateMappedFilesAsync(mappingPaths, projectRoot) {
|
|
|
1332
1332
|
const normalized = mappingPaths.map(normalizeMappingPath).filter((p) => p !== "");
|
|
1333
1333
|
return expandMappingPaths(projectRoot, normalized);
|
|
1334
1334
|
}
|
|
1335
|
-
async function
|
|
1335
|
+
async function loadHookModule(params) {
|
|
1336
1336
|
ensureLoaderRegistered();
|
|
1337
|
-
const { aspectDir,
|
|
1338
|
-
const
|
|
1339
|
-
const
|
|
1337
|
+
const { aspectDir, projectRoot, filename } = params;
|
|
1338
|
+
const resolveFailedCode = params.resolveFailedCode ?? "STRUCTURE_LOADER_RESOLVE_FAILED";
|
|
1339
|
+
const aspectDirAbs = path8.isAbsolute(aspectDir) ? aspectDir : path8.resolve(projectRoot, aspectDir);
|
|
1340
|
+
const modulePath = path8.join(aspectDirAbs, filename);
|
|
1341
|
+
try {
|
|
1342
|
+
return await import(pathToFileURL2(modulePath).href);
|
|
1343
|
+
} catch (err) {
|
|
1344
|
+
throw new StructureRunnerError(resolveFailedCode, {
|
|
1345
|
+
what: `Failed to load ${filename} at ${modulePath}: ${err.message}`,
|
|
1346
|
+
why: `The runner dynamically imports the aspect's ${filename} before invoking it.`,
|
|
1347
|
+
next: `Ensure ${filename} exists at the aspect directory and has no unresolved imports.`
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
async function buildUnitCtx(params) {
|
|
1352
|
+
const { aspectId, nodePath, graph, projectRoot, astCache, touchedFiles, subjectScope } = params;
|
|
1340
1353
|
const node = graph.nodes.get(nodePath);
|
|
1341
1354
|
if (!node) {
|
|
1342
1355
|
throw new StructureRunnerError("STRUCTURE_NODE_MISSING", {
|
|
@@ -1345,26 +1358,7 @@ async function runStructureAspect(params) {
|
|
|
1345
1358
|
next: `Pass an existing node path, or add the node to the graph.`
|
|
1346
1359
|
});
|
|
1347
1360
|
}
|
|
1348
|
-
|
|
1349
|
-
const checkPath = path8.join(aspectDirAbs, "check.mjs");
|
|
1350
|
-
let mod;
|
|
1351
|
-
try {
|
|
1352
|
-
mod = await import(pathToFileURL2(checkPath).href);
|
|
1353
|
-
} catch (err) {
|
|
1354
|
-
throw new StructureRunnerError("STRUCTURE_LOADER_RESOLVE_FAILED", {
|
|
1355
|
-
what: `Failed to load check.mjs at ${checkPath}: ${err.message}`,
|
|
1356
|
-
why: `The runner dynamically imports the aspect's check.mjs before invoking it.`,
|
|
1357
|
-
next: `Ensure check.mjs exists at the aspect directory and has no unresolved imports.`
|
|
1358
|
-
});
|
|
1359
|
-
}
|
|
1360
|
-
const exportCheck = validateCheckModuleExport(mod, {
|
|
1361
|
-
codePrefix: "STRUCTURE",
|
|
1362
|
-
runnerLabel: `aspect '${aspectId}'`
|
|
1363
|
-
});
|
|
1364
|
-
if (!exportCheck.ok) {
|
|
1365
|
-
throw new StructureRunnerError(exportCheck.code, exportCheck.message);
|
|
1366
|
-
}
|
|
1367
|
-
const checkFn = mod.check;
|
|
1361
|
+
void aspectId;
|
|
1368
1362
|
const allowedSet = collectAllowedReadsForAspect(nodePath, graph);
|
|
1369
1363
|
const recorder = new ObservationRecorder();
|
|
1370
1364
|
const ownFilesRaw = (node.meta.mapping ?? []).map(normalizeMappingPath).filter((p) => p !== "");
|
|
@@ -1406,6 +1400,11 @@ async function runStructureAspect(params) {
|
|
|
1406
1400
|
ports: node.meta.ports ?? {}
|
|
1407
1401
|
},
|
|
1408
1402
|
files: ctxFilesEnriched,
|
|
1403
|
+
// ctx.subject is the unit's subject file(s): for the deterministic whole-node
|
|
1404
|
+
// case it is the SAME array reference as ctx.files; for a per:file unit it is
|
|
1405
|
+
// exactly the narrowed subject view (also ctx.files here). Identical reference
|
|
1406
|
+
// in both branches keeps the alias contract.
|
|
1407
|
+
subject: ctxFilesEnriched,
|
|
1409
1408
|
fs: ctxFs,
|
|
1410
1409
|
graph: ctxGraph,
|
|
1411
1410
|
parseAst: parsers.parseAst,
|
|
@@ -1427,6 +1426,32 @@ async function runStructureAspect(params) {
|
|
|
1427
1426
|
}
|
|
1428
1427
|
}
|
|
1429
1428
|
await prewarmupAstCache({ astCache, projectRoot, files: astInputSet });
|
|
1429
|
+
return { ctx, recorder, node, subjectFiles, ownFiles, astInputSet };
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// src/structure/runner.ts
|
|
1433
|
+
async function runStructureAspect(params) {
|
|
1434
|
+
const { aspectDir, aspectId, nodePath, graph, projectRoot, subjectScope } = params;
|
|
1435
|
+
const astCache = params.parseCache ?? /* @__PURE__ */ new Map();
|
|
1436
|
+
const touchedFiles = [];
|
|
1437
|
+
const mod = await loadHookModule({ aspectDir, projectRoot, filename: "check.mjs" });
|
|
1438
|
+
const exportCheck = validateCheckModuleExport(mod, {
|
|
1439
|
+
codePrefix: "STRUCTURE",
|
|
1440
|
+
runnerLabel: `aspect '${aspectId}'`
|
|
1441
|
+
});
|
|
1442
|
+
if (!exportCheck.ok) {
|
|
1443
|
+
throw new StructureRunnerError(exportCheck.code, exportCheck.message);
|
|
1444
|
+
}
|
|
1445
|
+
const checkFn = mod.check;
|
|
1446
|
+
const { ctx, recorder, ownFiles, astInputSet } = await buildUnitCtx({
|
|
1447
|
+
aspectId,
|
|
1448
|
+
nodePath,
|
|
1449
|
+
graph,
|
|
1450
|
+
projectRoot,
|
|
1451
|
+
astCache,
|
|
1452
|
+
touchedFiles,
|
|
1453
|
+
subjectScope
|
|
1454
|
+
});
|
|
1430
1455
|
let raw;
|
|
1431
1456
|
try {
|
|
1432
1457
|
raw = checkFn(ctx);
|