@px-lsp/protocol 0.1.0 → 0.2.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/LICENSE +674 -674
- package/README.md +25 -25
- package/dist/calendar.d.ts +69 -0
- package/dist/calendar.js +183 -0
- package/dist/calendarLoc.d.ts +60 -0
- package/dist/calendarLoc.js +105 -0
- package/dist/descriptorMod.d.ts +21 -0
- package/dist/descriptorMod.js +61 -5
- package/dist/kinds.d.ts +72 -0
- package/dist/kinds.js +186 -0
- package/dist/protocol.d.ts +180 -4
- package/dist/protocol.js +37 -2
- package/dist/workshopMeta.d.ts +42 -0
- package/dist/workshopMeta.js +151 -0
- package/package.json +1 -1
- package/src/arrays.ts +16 -16
- package/src/calendar.ts +184 -0
- package/src/calendarLoc.ts +166 -0
- package/src/constants.ts +12 -12
- package/src/descriptorMetadata.ts +101 -101
- package/src/descriptorMod.ts +414 -354
- package/src/errorLogParser.ts +136 -136
- package/src/fsWalk.ts +126 -126
- package/src/kinds.ts +205 -0
- package/src/locProperties.ts +43 -43
- package/src/locRefs.ts +38 -38
- package/src/modName.ts +18 -18
- package/src/protocol.ts +1669 -1459
- package/src/regex.ts +19 -19
- package/src/suppression.ts +178 -178
- package/src/tigerParser.ts +79 -79
- package/src/translationCore.ts +140 -140
- package/src/types.ts +90 -90
- package/src/workshopMeta.ts +132 -0
package/src/regex.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* One correct copy of the regex-escape both sides need. It lived inline at five
|
|
3
|
-
* call sites and one of them had an extra backslash, which silently turned the
|
|
4
|
-
* escape into a no-op (it matched a metacharacter followed by two literal
|
|
5
|
-
* backslashes, so nothing was ever escaped).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/** Escape every regex metacharacter in `literal` so it matches itself. */
|
|
9
|
-
export function escapeRegExp(literal: string): string {
|
|
10
|
-
return literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* A pattern matching `name` only as a whole script identifier: not when it is
|
|
15
|
-
* a substring of a longer name, and not across a dot-chain segment boundary.
|
|
16
|
-
*/
|
|
17
|
-
export function wholeNamePattern(name: string): string {
|
|
18
|
-
return `(?<![A-Za-z0-9_.\\-])${escapeRegExp(name)}(?![A-Za-z0-9_.\\-])`;
|
|
19
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* One correct copy of the regex-escape both sides need. It lived inline at five
|
|
3
|
+
* call sites and one of them had an extra backslash, which silently turned the
|
|
4
|
+
* escape into a no-op (it matched a metacharacter followed by two literal
|
|
5
|
+
* backslashes, so nothing was ever escaped).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Escape every regex metacharacter in `literal` so it matches itself. */
|
|
9
|
+
export function escapeRegExp(literal: string): string {
|
|
10
|
+
return literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A pattern matching `name` only as a whole script identifier: not when it is
|
|
15
|
+
* a substring of a longer name, and not across a dot-chain segment boundary.
|
|
16
|
+
*/
|
|
17
|
+
export function wholeNamePattern(name: string): string {
|
|
18
|
+
return `(?<![A-Za-z0-9_.\\-])${escapeRegExp(name)}(?![A-Za-z0-9_.\\-])`;
|
|
19
|
+
}
|
package/src/suppression.ts
CHANGED
|
@@ -1,178 +1,178 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Diagnostic suppression, shared by the server (own structural/loc diagnostics)
|
|
3
|
-
* and the client (tiger-forwarded reports) so one habit works across both tools.
|
|
4
|
-
*
|
|
5
|
-
* No `vscode` imports: plain data in, plain predicates out. Everything here is
|
|
6
|
-
* fail-soft — bad setting values or malformed comments are ignored, never thrown.
|
|
7
|
-
*
|
|
8
|
-
* Two mechanisms:
|
|
9
|
-
* 1. Settings: the diagnostics.ignore setting (diagnostic codes) and
|
|
10
|
-
* the diagnostics.ignorePatterns setting (globs on the workspace-relative path).
|
|
11
|
-
* 2. Inline comments: `# px:ignore <code…>` (same line) and
|
|
12
|
-
* `# px:ignore-next-line <code…>` (following line); a bare form with no
|
|
13
|
-
* codes suppresses every diagnostic on the target line. A trailing
|
|
14
|
-
* `-- <rationale>` is allowed and ignored.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Settings-driven filter. `ignore` matches a diagnostic's code (our stable
|
|
19
|
-
* codes, or tiger's `key`); `ignorePatterns` matches globs against the
|
|
20
|
-
* workspace-relative file path.
|
|
21
|
-
*/
|
|
22
|
-
export interface DiagnosticIgnoreConfig {
|
|
23
|
-
/** Diagnostic codes to drop everywhere. */
|
|
24
|
-
ignore: string[];
|
|
25
|
-
/** Glob patterns matched against the workspace-relative (forward-slash) path. */
|
|
26
|
-
ignorePatterns: string[];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Normalize a raw settings array: strings only, trimmed, empties dropped. */
|
|
30
|
-
export function sanitizeStringList(value: unknown): string[] {
|
|
31
|
-
if (!Array.isArray(value)) return [];
|
|
32
|
-
const out: string[] = [];
|
|
33
|
-
for (const v of value) {
|
|
34
|
-
if (typeof v !== "string") continue;
|
|
35
|
-
const t = v.trim();
|
|
36
|
-
if (t !== "") out.push(t);
|
|
37
|
-
}
|
|
38
|
-
return out;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Tiny `*`/`**` glob matcher (no dependency). `*` matches within a path segment,
|
|
43
|
-
* `**` matches across segments (including `/`). Matching is done on
|
|
44
|
-
* forward-slash paths and is case-insensitive (Windows-friendly). A pattern with
|
|
45
|
-
* no slash also matches against the basename, so `*.txt` works like a gitignore
|
|
46
|
-
* entry. Returns false on any malformed pattern.
|
|
47
|
-
*/
|
|
48
|
-
export function globMatch(pattern: string, filePath: string): boolean {
|
|
49
|
-
const p = pattern.replace(/\\/g, "/").toLowerCase();
|
|
50
|
-
const f = filePath.replace(/\\/g, "/").replace(/^\/+/, "").toLowerCase();
|
|
51
|
-
if (p === "") return false;
|
|
52
|
-
try {
|
|
53
|
-
const re = new RegExp("^" + globToRegExpSource(p) + "$");
|
|
54
|
-
if (re.test(f)) return true;
|
|
55
|
-
// Slash-free patterns also match the basename (gitignore-style convenience).
|
|
56
|
-
if (!p.includes("/")) {
|
|
57
|
-
const base = f.slice(f.lastIndexOf("/") + 1);
|
|
58
|
-
return re.test(base);
|
|
59
|
-
}
|
|
60
|
-
return false;
|
|
61
|
-
} catch {
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Translate a glob (already lowercased, forward-slashed) into a regex source. */
|
|
67
|
-
function globToRegExpSource(glob: string): string {
|
|
68
|
-
let out = "";
|
|
69
|
-
for (let i = 0; i < glob.length; i++) {
|
|
70
|
-
const c = glob[i];
|
|
71
|
-
if (c === "*") {
|
|
72
|
-
if (glob[i + 1] === "*") {
|
|
73
|
-
// `**` — cross segments, optionally swallowing a trailing slash.
|
|
74
|
-
i++;
|
|
75
|
-
if (glob[i + 1] === "/") {
|
|
76
|
-
i++;
|
|
77
|
-
out += "(?:.*/)?";
|
|
78
|
-
} else {
|
|
79
|
-
out += ".*";
|
|
80
|
-
}
|
|
81
|
-
} else {
|
|
82
|
-
out += "[^/]*";
|
|
83
|
-
}
|
|
84
|
-
} else if (c === "?") {
|
|
85
|
-
out += "[^/]";
|
|
86
|
-
} else if ("\\^$.|+()[]{}".includes(c)) {
|
|
87
|
-
out += "\\" + c;
|
|
88
|
-
} else {
|
|
89
|
-
out += c;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return out;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** True when a diagnostic with `code` in `filePath` should be dropped by settings. */
|
|
96
|
-
export function isIgnoredByConfig(
|
|
97
|
-
cfg: DiagnosticIgnoreConfig,
|
|
98
|
-
code: string | undefined,
|
|
99
|
-
relPath: string
|
|
100
|
-
): boolean {
|
|
101
|
-
if (code !== undefined && cfg.ignore.includes(code)) return true;
|
|
102
|
-
for (const pattern of cfg.ignorePatterns) {
|
|
103
|
-
if (globMatch(pattern, relPath)) return true;
|
|
104
|
-
}
|
|
105
|
-
return false;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Inline suppression map for a file, keyed by 0-based line number. A `null`
|
|
110
|
-
* value means "suppress every code on this line"; an array means "suppress only
|
|
111
|
-
* these codes". Built by scanning comment lines once when publishing.
|
|
112
|
-
*/
|
|
113
|
-
export type InlineSuppressions = Map<number, string[] | null>;
|
|
114
|
-
|
|
115
|
-
const IGNORE_RE = /#\s*px:ignore(-next-line)?\b([^\n]*)/i;
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Scan a document's text for `# px:ignore[-next-line] <code…>` comments.
|
|
119
|
-
* Cheap: only lines containing `px:ignore` are parsed. `-next-line` targets
|
|
120
|
-
* the following line; the plain form targets its own line.
|
|
121
|
-
*/
|
|
122
|
-
export function scanInlineSuppressions(text: string): InlineSuppressions {
|
|
123
|
-
const map: InlineSuppressions = new Map();
|
|
124
|
-
if (!text.includes("px:ignore")) return map;
|
|
125
|
-
const lines = text.split(/\r?\n/);
|
|
126
|
-
for (let i = 0; i < lines.length; i++) {
|
|
127
|
-
const line = lines[i];
|
|
128
|
-
// A comment can trail script on the same line; only look after the `#`.
|
|
129
|
-
const hash = line.indexOf("#");
|
|
130
|
-
if (hash < 0) continue;
|
|
131
|
-
const m = IGNORE_RE.exec(line.slice(hash));
|
|
132
|
-
if (!m) continue;
|
|
133
|
-
const target = m[1] ? i + 1 : i;
|
|
134
|
-
const codes = parseCodes(m[2]);
|
|
135
|
-
mergeSuppression(map, target, codes.length === 0 ? null : codes);
|
|
136
|
-
}
|
|
137
|
-
return map;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* The codes following the marker, stopping at a `--` rationale. Writing a
|
|
142
|
-
* reason is the natural instinct, and without the cut-off every word of it
|
|
143
|
-
* parsed as a code — turning a suppression that matched everything into one
|
|
144
|
-
* that matched nothing, silently. Codes are kebab-case slugs
|
|
145
|
-
* (`unclosed-brace`, `loc-no-header`), so a leading `-` can only be the
|
|
146
|
-
* separator.
|
|
147
|
-
*/
|
|
148
|
-
function parseCodes(rest: string): string[] {
|
|
149
|
-
const out: string[] = [];
|
|
150
|
-
for (const token of rest.trim().split(/\s+/)) {
|
|
151
|
-
if (token === "") continue;
|
|
152
|
-
if (token.startsWith("-")) break; // `-- because the game allows it`
|
|
153
|
-
out.push(token);
|
|
154
|
-
}
|
|
155
|
-
return out;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function mergeSuppression(map: InlineSuppressions, line: number, codes: string[] | null): void {
|
|
159
|
-
const existing = map.get(line);
|
|
160
|
-
if (existing === undefined) {
|
|
161
|
-
map.set(line, codes);
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
// `null` (suppress-all) wins; otherwise union the code lists.
|
|
165
|
-
if (existing === null || codes === null) {
|
|
166
|
-
map.set(line, null);
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
map.set(line, [...existing, ...codes]);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/** True when line `line` has an inline suppression covering `code`. */
|
|
173
|
-
export function isSuppressedInline(map: InlineSuppressions, line: number, code: string | undefined): boolean {
|
|
174
|
-
if (!map.has(line)) return false;
|
|
175
|
-
const codes = map.get(line) ?? null;
|
|
176
|
-
if (codes === null) return true; // bare `# px:ignore` suppresses all
|
|
177
|
-
return code !== undefined && codes.includes(code);
|
|
178
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Diagnostic suppression, shared by the server (own structural/loc diagnostics)
|
|
3
|
+
* and the client (tiger-forwarded reports) so one habit works across both tools.
|
|
4
|
+
*
|
|
5
|
+
* No `vscode` imports: plain data in, plain predicates out. Everything here is
|
|
6
|
+
* fail-soft — bad setting values or malformed comments are ignored, never thrown.
|
|
7
|
+
*
|
|
8
|
+
* Two mechanisms:
|
|
9
|
+
* 1. Settings: the diagnostics.ignore setting (diagnostic codes) and
|
|
10
|
+
* the diagnostics.ignorePatterns setting (globs on the workspace-relative path).
|
|
11
|
+
* 2. Inline comments: `# px:ignore <code…>` (same line) and
|
|
12
|
+
* `# px:ignore-next-line <code…>` (following line); a bare form with no
|
|
13
|
+
* codes suppresses every diagnostic on the target line. A trailing
|
|
14
|
+
* `-- <rationale>` is allowed and ignored.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Settings-driven filter. `ignore` matches a diagnostic's code (our stable
|
|
19
|
+
* codes, or tiger's `key`); `ignorePatterns` matches globs against the
|
|
20
|
+
* workspace-relative file path.
|
|
21
|
+
*/
|
|
22
|
+
export interface DiagnosticIgnoreConfig {
|
|
23
|
+
/** Diagnostic codes to drop everywhere. */
|
|
24
|
+
ignore: string[];
|
|
25
|
+
/** Glob patterns matched against the workspace-relative (forward-slash) path. */
|
|
26
|
+
ignorePatterns: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Normalize a raw settings array: strings only, trimmed, empties dropped. */
|
|
30
|
+
export function sanitizeStringList(value: unknown): string[] {
|
|
31
|
+
if (!Array.isArray(value)) return [];
|
|
32
|
+
const out: string[] = [];
|
|
33
|
+
for (const v of value) {
|
|
34
|
+
if (typeof v !== "string") continue;
|
|
35
|
+
const t = v.trim();
|
|
36
|
+
if (t !== "") out.push(t);
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Tiny `*`/`**` glob matcher (no dependency). `*` matches within a path segment,
|
|
43
|
+
* `**` matches across segments (including `/`). Matching is done on
|
|
44
|
+
* forward-slash paths and is case-insensitive (Windows-friendly). A pattern with
|
|
45
|
+
* no slash also matches against the basename, so `*.txt` works like a gitignore
|
|
46
|
+
* entry. Returns false on any malformed pattern.
|
|
47
|
+
*/
|
|
48
|
+
export function globMatch(pattern: string, filePath: string): boolean {
|
|
49
|
+
const p = pattern.replace(/\\/g, "/").toLowerCase();
|
|
50
|
+
const f = filePath.replace(/\\/g, "/").replace(/^\/+/, "").toLowerCase();
|
|
51
|
+
if (p === "") return false;
|
|
52
|
+
try {
|
|
53
|
+
const re = new RegExp("^" + globToRegExpSource(p) + "$");
|
|
54
|
+
if (re.test(f)) return true;
|
|
55
|
+
// Slash-free patterns also match the basename (gitignore-style convenience).
|
|
56
|
+
if (!p.includes("/")) {
|
|
57
|
+
const base = f.slice(f.lastIndexOf("/") + 1);
|
|
58
|
+
return re.test(base);
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Translate a glob (already lowercased, forward-slashed) into a regex source. */
|
|
67
|
+
function globToRegExpSource(glob: string): string {
|
|
68
|
+
let out = "";
|
|
69
|
+
for (let i = 0; i < glob.length; i++) {
|
|
70
|
+
const c = glob[i];
|
|
71
|
+
if (c === "*") {
|
|
72
|
+
if (glob[i + 1] === "*") {
|
|
73
|
+
// `**` — cross segments, optionally swallowing a trailing slash.
|
|
74
|
+
i++;
|
|
75
|
+
if (glob[i + 1] === "/") {
|
|
76
|
+
i++;
|
|
77
|
+
out += "(?:.*/)?";
|
|
78
|
+
} else {
|
|
79
|
+
out += ".*";
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
out += "[^/]*";
|
|
83
|
+
}
|
|
84
|
+
} else if (c === "?") {
|
|
85
|
+
out += "[^/]";
|
|
86
|
+
} else if ("\\^$.|+()[]{}".includes(c)) {
|
|
87
|
+
out += "\\" + c;
|
|
88
|
+
} else {
|
|
89
|
+
out += c;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** True when a diagnostic with `code` in `filePath` should be dropped by settings. */
|
|
96
|
+
export function isIgnoredByConfig(
|
|
97
|
+
cfg: DiagnosticIgnoreConfig,
|
|
98
|
+
code: string | undefined,
|
|
99
|
+
relPath: string
|
|
100
|
+
): boolean {
|
|
101
|
+
if (code !== undefined && cfg.ignore.includes(code)) return true;
|
|
102
|
+
for (const pattern of cfg.ignorePatterns) {
|
|
103
|
+
if (globMatch(pattern, relPath)) return true;
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Inline suppression map for a file, keyed by 0-based line number. A `null`
|
|
110
|
+
* value means "suppress every code on this line"; an array means "suppress only
|
|
111
|
+
* these codes". Built by scanning comment lines once when publishing.
|
|
112
|
+
*/
|
|
113
|
+
export type InlineSuppressions = Map<number, string[] | null>;
|
|
114
|
+
|
|
115
|
+
const IGNORE_RE = /#\s*px:ignore(-next-line)?\b([^\n]*)/i;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Scan a document's text for `# px:ignore[-next-line] <code…>` comments.
|
|
119
|
+
* Cheap: only lines containing `px:ignore` are parsed. `-next-line` targets
|
|
120
|
+
* the following line; the plain form targets its own line.
|
|
121
|
+
*/
|
|
122
|
+
export function scanInlineSuppressions(text: string): InlineSuppressions {
|
|
123
|
+
const map: InlineSuppressions = new Map();
|
|
124
|
+
if (!text.includes("px:ignore")) return map;
|
|
125
|
+
const lines = text.split(/\r?\n/);
|
|
126
|
+
for (let i = 0; i < lines.length; i++) {
|
|
127
|
+
const line = lines[i];
|
|
128
|
+
// A comment can trail script on the same line; only look after the `#`.
|
|
129
|
+
const hash = line.indexOf("#");
|
|
130
|
+
if (hash < 0) continue;
|
|
131
|
+
const m = IGNORE_RE.exec(line.slice(hash));
|
|
132
|
+
if (!m) continue;
|
|
133
|
+
const target = m[1] ? i + 1 : i;
|
|
134
|
+
const codes = parseCodes(m[2]);
|
|
135
|
+
mergeSuppression(map, target, codes.length === 0 ? null : codes);
|
|
136
|
+
}
|
|
137
|
+
return map;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The codes following the marker, stopping at a `--` rationale. Writing a
|
|
142
|
+
* reason is the natural instinct, and without the cut-off every word of it
|
|
143
|
+
* parsed as a code — turning a suppression that matched everything into one
|
|
144
|
+
* that matched nothing, silently. Codes are kebab-case slugs
|
|
145
|
+
* (`unclosed-brace`, `loc-no-header`), so a leading `-` can only be the
|
|
146
|
+
* separator.
|
|
147
|
+
*/
|
|
148
|
+
function parseCodes(rest: string): string[] {
|
|
149
|
+
const out: string[] = [];
|
|
150
|
+
for (const token of rest.trim().split(/\s+/)) {
|
|
151
|
+
if (token === "") continue;
|
|
152
|
+
if (token.startsWith("-")) break; // `-- because the game allows it`
|
|
153
|
+
out.push(token);
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function mergeSuppression(map: InlineSuppressions, line: number, codes: string[] | null): void {
|
|
159
|
+
const existing = map.get(line);
|
|
160
|
+
if (existing === undefined) {
|
|
161
|
+
map.set(line, codes);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
// `null` (suppress-all) wins; otherwise union the code lists.
|
|
165
|
+
if (existing === null || codes === null) {
|
|
166
|
+
map.set(line, null);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
map.set(line, [...existing, ...codes]);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** True when line `line` has an inline suppression covering `code`. */
|
|
173
|
+
export function isSuppressedInline(map: InlineSuppressions, line: number, code: string | undefined): boolean {
|
|
174
|
+
if (!map.has(line)) return false;
|
|
175
|
+
const codes = map.get(line) ?? null;
|
|
176
|
+
if (codes === null) return true; // bare `# px:ignore` suppresses all
|
|
177
|
+
return code !== undefined && codes.includes(code);
|
|
178
|
+
}
|
package/src/tigerParser.ts
CHANGED
|
@@ -1,79 +1,79 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Parser for tiger `--json` reports (the Paradox script validator family).
|
|
3
|
-
*
|
|
4
|
-
* Kept separate from the process management in tiger.ts so it stays free of
|
|
5
|
-
* `vscode` imports and defensively tolerant of format drift between tiger
|
|
6
|
-
* releases: unknown fields are ignored, malformed entries are skipped.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
export interface TigerLocation {
|
|
10
|
-
path: string;
|
|
11
|
-
fullpath?: string;
|
|
12
|
-
/** 1-based, may be missing for file-level reports. */
|
|
13
|
-
linenr?: number;
|
|
14
|
-
/** 1-based. */
|
|
15
|
-
column?: number;
|
|
16
|
-
length?: number;
|
|
17
|
-
tag?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface TigerReport {
|
|
21
|
-
severity: string;
|
|
22
|
-
/** tiger also rates how sure it is: weak | reasonable | strong. */
|
|
23
|
-
confidence?: string;
|
|
24
|
-
key: string;
|
|
25
|
-
message: string;
|
|
26
|
-
info?: string;
|
|
27
|
-
locations: TigerLocation[];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Parse tiger's JSON output. Returns null if no JSON array can be found at all. */
|
|
31
|
-
export function parseTigerJson(stdout: string): TigerReport[] | null {
|
|
32
|
-
let raw: unknown;
|
|
33
|
-
try {
|
|
34
|
-
raw = JSON.parse(stdout);
|
|
35
|
-
} catch {
|
|
36
|
-
// tiger may print progress noise before the JSON; try from the first '['.
|
|
37
|
-
const start = stdout.indexOf("[");
|
|
38
|
-
if (start < 0) return null;
|
|
39
|
-
try {
|
|
40
|
-
raw = JSON.parse(stdout.slice(start));
|
|
41
|
-
} catch {
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
if (!Array.isArray(raw)) return null;
|
|
46
|
-
|
|
47
|
-
const reports: TigerReport[] = [];
|
|
48
|
-
for (const entry of raw) {
|
|
49
|
-
if (typeof entry !== "object" || entry === null) continue;
|
|
50
|
-
const e = entry as Record<string, unknown>;
|
|
51
|
-
const message = typeof e.message === "string" ? e.message : null;
|
|
52
|
-
const locationsRaw = Array.isArray(e.locations) ? e.locations : [];
|
|
53
|
-
if (message === null) continue;
|
|
54
|
-
const locations: TigerLocation[] = [];
|
|
55
|
-
for (const locRaw of locationsRaw) {
|
|
56
|
-
if (typeof locRaw !== "object" || locRaw === null) continue;
|
|
57
|
-
const l = locRaw as Record<string, unknown>;
|
|
58
|
-
const p = typeof l.fullpath === "string" ? l.fullpath : typeof l.path === "string" ? l.path : null;
|
|
59
|
-
if (p === null) continue;
|
|
60
|
-
const loc: TigerLocation = { path: typeof l.path === "string" ? l.path : p };
|
|
61
|
-
if (typeof l.fullpath === "string") loc.fullpath = l.fullpath;
|
|
62
|
-
const linenr = l.linenr ?? l.line;
|
|
63
|
-
if (typeof linenr === "number") loc.linenr = linenr;
|
|
64
|
-
if (typeof l.column === "number") loc.column = l.column;
|
|
65
|
-
if (typeof l.length === "number") loc.length = l.length;
|
|
66
|
-
if (typeof l.tag === "string") loc.tag = l.tag;
|
|
67
|
-
locations.push(loc);
|
|
68
|
-
}
|
|
69
|
-
reports.push({
|
|
70
|
-
severity: typeof e.severity === "string" ? e.severity : "warning",
|
|
71
|
-
confidence: typeof e.confidence === "string" ? e.confidence : undefined,
|
|
72
|
-
key: typeof e.key === "string" ? e.key : "unknown",
|
|
73
|
-
message,
|
|
74
|
-
info: typeof e.info === "string" ? e.info : undefined,
|
|
75
|
-
locations,
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
return reports;
|
|
79
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Parser for tiger `--json` reports (the Paradox script validator family).
|
|
3
|
+
*
|
|
4
|
+
* Kept separate from the process management in tiger.ts so it stays free of
|
|
5
|
+
* `vscode` imports and defensively tolerant of format drift between tiger
|
|
6
|
+
* releases: unknown fields are ignored, malformed entries are skipped.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface TigerLocation {
|
|
10
|
+
path: string;
|
|
11
|
+
fullpath?: string;
|
|
12
|
+
/** 1-based, may be missing for file-level reports. */
|
|
13
|
+
linenr?: number;
|
|
14
|
+
/** 1-based. */
|
|
15
|
+
column?: number;
|
|
16
|
+
length?: number;
|
|
17
|
+
tag?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TigerReport {
|
|
21
|
+
severity: string;
|
|
22
|
+
/** tiger also rates how sure it is: weak | reasonable | strong. */
|
|
23
|
+
confidence?: string;
|
|
24
|
+
key: string;
|
|
25
|
+
message: string;
|
|
26
|
+
info?: string;
|
|
27
|
+
locations: TigerLocation[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Parse tiger's JSON output. Returns null if no JSON array can be found at all. */
|
|
31
|
+
export function parseTigerJson(stdout: string): TigerReport[] | null {
|
|
32
|
+
let raw: unknown;
|
|
33
|
+
try {
|
|
34
|
+
raw = JSON.parse(stdout);
|
|
35
|
+
} catch {
|
|
36
|
+
// tiger may print progress noise before the JSON; try from the first '['.
|
|
37
|
+
const start = stdout.indexOf("[");
|
|
38
|
+
if (start < 0) return null;
|
|
39
|
+
try {
|
|
40
|
+
raw = JSON.parse(stdout.slice(start));
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (!Array.isArray(raw)) return null;
|
|
46
|
+
|
|
47
|
+
const reports: TigerReport[] = [];
|
|
48
|
+
for (const entry of raw) {
|
|
49
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
50
|
+
const e = entry as Record<string, unknown>;
|
|
51
|
+
const message = typeof e.message === "string" ? e.message : null;
|
|
52
|
+
const locationsRaw = Array.isArray(e.locations) ? e.locations : [];
|
|
53
|
+
if (message === null) continue;
|
|
54
|
+
const locations: TigerLocation[] = [];
|
|
55
|
+
for (const locRaw of locationsRaw) {
|
|
56
|
+
if (typeof locRaw !== "object" || locRaw === null) continue;
|
|
57
|
+
const l = locRaw as Record<string, unknown>;
|
|
58
|
+
const p = typeof l.fullpath === "string" ? l.fullpath : typeof l.path === "string" ? l.path : null;
|
|
59
|
+
if (p === null) continue;
|
|
60
|
+
const loc: TigerLocation = { path: typeof l.path === "string" ? l.path : p };
|
|
61
|
+
if (typeof l.fullpath === "string") loc.fullpath = l.fullpath;
|
|
62
|
+
const linenr = l.linenr ?? l.line;
|
|
63
|
+
if (typeof linenr === "number") loc.linenr = linenr;
|
|
64
|
+
if (typeof l.column === "number") loc.column = l.column;
|
|
65
|
+
if (typeof l.length === "number") loc.length = l.length;
|
|
66
|
+
if (typeof l.tag === "string") loc.tag = l.tag;
|
|
67
|
+
locations.push(loc);
|
|
68
|
+
}
|
|
69
|
+
reports.push({
|
|
70
|
+
severity: typeof e.severity === "string" ? e.severity : "warning",
|
|
71
|
+
confidence: typeof e.confidence === "string" ? e.confidence : undefined,
|
|
72
|
+
key: typeof e.key === "string" ? e.key : "unknown",
|
|
73
|
+
message,
|
|
74
|
+
info: typeof e.info === "string" ? e.info : undefined,
|
|
75
|
+
locations,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return reports;
|
|
79
|
+
}
|