@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/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
+ }
@@ -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
+ }
@@ -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
+ }