@jterrazz/test 15.0.0 → 15.1.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.
- package/README.md +1 -1
- package/dist/ambiguity.js +1 -1
- package/dist/appium.adapter.js +33 -24
- package/dist/artifacts.js +503 -525
- package/dist/checker.js +60 -59
- package/dist/index.d.ts +587 -660
- package/dist/index.js +191 -188
- package/dist/match.js +6 -16
- package/dist/oxlint.cjs +538 -525
- package/dist/oxlint.d.cts +7064 -15
- package/dist/oxlint.d.ts +7064 -15
- package/dist/oxlint.js +538 -525
- package/dist/playwright.adapter.js +36 -14
- package/dist/queue.js +27 -28
- package/dist/spec-document.js +19 -25
- package/dist/vitest.d.ts +6 -6
- package/dist/vitest.js +2 -2
- package/package.json +10 -7
package/dist/checker.js
CHANGED
|
@@ -218,7 +218,7 @@ function lineAt(text, index) {
|
|
|
218
218
|
* Every quoted string and simple template literal in a source. Over-collection
|
|
219
219
|
* only ever *hides* a dead-fixture finding, so a greedy regex is safe here.
|
|
220
220
|
*/
|
|
221
|
-
const STRING_LITERAL = /'(?<single>(?:[^'\\\n]|\\.)*)'|"(?<double>(?:[^"\\\n]|\\.)*)"|`(?<template>(?:[^`$\\]|\\.)*)`/
|
|
221
|
+
const STRING_LITERAL = /'(?<single>(?:[^'\\\n]|\\.)*)'|"(?<double>(?:[^"\\\n]|\\.)*)"|`(?<template>(?:[^`$\\]|\\.)*)`/gu;
|
|
222
222
|
function collectLiterals(text) {
|
|
223
223
|
const found = /* @__PURE__ */ new Set();
|
|
224
224
|
for (const match of text.matchAll(STRING_LITERAL)) {
|
|
@@ -233,12 +233,12 @@ function collectLiterals(text) {
|
|
|
233
233
|
* variable makes the reference set incomplete, so the feature is reported at
|
|
234
234
|
* warn level instead of error (a false positive would be worse than a miss).
|
|
235
235
|
*/
|
|
236
|
-
const FIXTURE_VERB_ARG = /(?:\.(?:seed|request|fixture)|\btoMatch)\s*\(\s*(?<arg>[^,)]*)/
|
|
236
|
+
const FIXTURE_VERB_ARG = /(?:\.(?:seed|request|fixture)|\btoMatch)\s*\(\s*(?<arg>[^,)]*)/gu;
|
|
237
237
|
function hasNonLiteralFixtureArg(text) {
|
|
238
238
|
for (const match of text.matchAll(FIXTURE_VERB_ARG)) {
|
|
239
239
|
const arg = (match.groups?.arg ?? "").trim();
|
|
240
240
|
if (arg.length === 0) continue;
|
|
241
|
-
if (!(/^'(?:[^'\\\n]|\\.)*'
|
|
241
|
+
if (!(/^'(?:[^'\\\n]|\\.)*'$/u.test(arg) || /^"(?:[^"\\\n]|\\.)*"$/u.test(arg) || /^`(?:[^`$\\]|\\.)*`$/u.test(arg))) return true;
|
|
242
242
|
}
|
|
243
243
|
return false;
|
|
244
244
|
}
|
|
@@ -252,18 +252,18 @@ function suppressedLines(text, id) {
|
|
|
252
252
|
const suppressed = /* @__PURE__ */ new Set();
|
|
253
253
|
const lines = text.split("\n");
|
|
254
254
|
const applies = (raw) => {
|
|
255
|
-
const ids = raw.replace(
|
|
255
|
+
const ids = raw.replace(/--.*$/u, "").trim().split(/[\s,]+/u);
|
|
256
256
|
return ids.includes(id) || ids.includes("*");
|
|
257
257
|
};
|
|
258
258
|
for (const [index, line] of lines.entries()) {
|
|
259
|
-
let match = /checker-disable-next-line\s+(?<ids>.*)$/
|
|
259
|
+
let match = /checker-disable-next-line\s+(?<ids>.*)$/iu.exec(line);
|
|
260
260
|
if (match !== null && applies(match.groups?.ids ?? "")) {
|
|
261
|
-
for (let next = index + 1; next < lines.length; next += 1) if (lines[next].trim().length > 0) {
|
|
261
|
+
for (let next = index + 1; next < lines.length; next += 1) if ((lines[next] ?? "").trim().length > 0) {
|
|
262
262
|
suppressed.add(next + 1);
|
|
263
263
|
break;
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
|
-
match = /checker-disable-line\s+(?<ids>.*)$/
|
|
266
|
+
match = /checker-disable-line\s+(?<ids>.*)$/iu.exec(line);
|
|
267
267
|
if (match !== null && applies(match.groups?.ids ?? "")) suppressed.add(index + 1);
|
|
268
268
|
}
|
|
269
269
|
return suppressed;
|
|
@@ -314,21 +314,21 @@ function splitTopLevel(body) {
|
|
|
314
314
|
/** The destructured export names of a `= await specification.<ctor>(` binding. */
|
|
315
315
|
function runnerExportsOf(text) {
|
|
316
316
|
const names = /* @__PURE__ */ new Set();
|
|
317
|
-
const decl = new RegExp(String.raw`\{(?<names>[^}]*)\}\s*=\s*await\s+specification\.(?:${CONSTRUCTORS})\s*\(`, "
|
|
317
|
+
const decl = new RegExp(String.raw`\{(?<names>[^}]*)\}\s*=\s*await\s+specification\.(?:${CONSTRUCTORS})\s*\(`, "gu");
|
|
318
318
|
for (const match of text.matchAll(decl)) for (const raw of (match.groups?.names ?? "").split(",")) {
|
|
319
|
-
const key = raw.split(":")[0].trim();
|
|
319
|
+
const key = (raw.split(":")[0] ?? "").trim();
|
|
320
320
|
if (key.length > 0 && !NON_RUNNER_BINDINGS.has(key)) names.add(key);
|
|
321
321
|
}
|
|
322
322
|
return names;
|
|
323
323
|
}
|
|
324
324
|
/** The leading callee identifier of an expression (`postgres()` → `postgres`). */
|
|
325
325
|
function leadingCallee(value) {
|
|
326
|
-
const match = /^(?<callee>[A-Za-z_$][\w$]*)\s*\(
|
|
326
|
+
const match = /^(?<callee>[A-Za-z_$][\w$]*)\s*\(/u.exec(value.trim());
|
|
327
327
|
return match === null ? match : match.groups?.callee ?? null;
|
|
328
328
|
}
|
|
329
329
|
/** SQL factory count of a `services:` record, or null if non-literal/absent. */
|
|
330
330
|
function sqlDatabaseCount(text) {
|
|
331
|
-
const keyword = /\bservices\s
|
|
331
|
+
const keyword = /\bservices\s*:/u.exec(text);
|
|
332
332
|
if (keyword === null) return null;
|
|
333
333
|
const braceIndex = text.indexOf("{", keyword.index + keyword[0].length);
|
|
334
334
|
if (braceIndex === -1 || text.slice(keyword.index + keyword[0].length, braceIndex).trim() !== "") return null;
|
|
@@ -348,9 +348,9 @@ function sqlDatabaseCount(text) {
|
|
|
348
348
|
function modelSpecFile(file) {
|
|
349
349
|
const text = readSource(file);
|
|
350
350
|
let dockerAware = false;
|
|
351
|
-
for (const match of text.matchAll(/specification\.cli\s*\(/
|
|
351
|
+
for (const match of text.matchAll(/specification\.cli\s*\(/gu)) {
|
|
352
352
|
const argsOpen = text.indexOf("(", match.index);
|
|
353
|
-
if (argsOpen !== -1 && /\bdocker\s
|
|
353
|
+
if (argsOpen !== -1 && /\bdocker\s*:/u.test(balancedParens(text, argsOpen))) dockerAware = true;
|
|
354
354
|
}
|
|
355
355
|
return {
|
|
356
356
|
dockerAware,
|
|
@@ -362,17 +362,18 @@ function modelSpecFile(file) {
|
|
|
362
362
|
/** Resolve a relative import specifier to a `.ts` path (else undefined). */
|
|
363
363
|
function resolveRelativeImport(fromFile, specifier) {
|
|
364
364
|
if (!specifier.startsWith(".")) return;
|
|
365
|
-
return resolve(dirname(fromFile), specifier).replace(/\.js
|
|
365
|
+
return resolve(dirname(fromFile), specifier).replace(/\.js$/u, ".ts");
|
|
366
366
|
}
|
|
367
367
|
/** Every `import { a, b as c } from '…'` binding in a file. */
|
|
368
368
|
function namedImports(text) {
|
|
369
369
|
const bindings = [];
|
|
370
|
-
for (const match of text.matchAll(/import\s+(?:type\s+)?\{(?<names>[^}]*)\}\s+from\s+['"](?<source>[^'"]+)['"]/
|
|
370
|
+
for (const match of text.matchAll(/import\s+(?:type\s+)?\{(?<names>[^}]*)\}\s+from\s+['"](?<source>[^'"]+)['"]/gu)) {
|
|
371
371
|
const source = match.groups?.source ?? "";
|
|
372
372
|
for (const raw of (match.groups?.names ?? "").split(",")) {
|
|
373
373
|
const part = raw.trim();
|
|
374
374
|
if (part.length === 0) continue;
|
|
375
|
-
const [name, local] = part.split(/\s+as\s+/).map((token) => token.trim());
|
|
375
|
+
const [name, local] = part.split(/\s+as\s+/u).map((token) => token.trim());
|
|
376
|
+
if (name === void 0) continue;
|
|
376
377
|
bindings.push({
|
|
377
378
|
local: local ?? name,
|
|
378
379
|
name,
|
|
@@ -405,7 +406,7 @@ function checkDockerRunnerAwaitUsing(rootDir) {
|
|
|
405
406
|
}
|
|
406
407
|
if (runners.size === 0) continue;
|
|
407
408
|
const suppressed = suppressedLines(readText$1(testFile), "b5");
|
|
408
|
-
for (const match of text.matchAll(/\b(?<kind>await\s+using|const|let|var)\s+[A-Za-z_$][\w$]*\s*=\s*await\s+(?<runner>[A-Za-z_$][\w$]*)\b[^;]*?\.exec\s*\(/
|
|
409
|
+
for (const match of text.matchAll(/\b(?<kind>await\s+using|const|let|var)\s+[A-Za-z_$][\w$]*\s*=\s*await\s+(?<runner>[A-Za-z_$][\w$]*)\b[^;]*?\.exec\s*\(/gu)) {
|
|
409
410
|
const kind = match.groups?.kind ?? "";
|
|
410
411
|
const runner = match.groups?.runner ?? "";
|
|
411
412
|
if (kind.startsWith("await using") || !runners.has(runner)) continue;
|
|
@@ -425,18 +426,18 @@ function checkDockerRunnerAwaitUsing(rootDir) {
|
|
|
425
426
|
/** Local inline runners a test declares itself (`specification.api(…)`). */
|
|
426
427
|
function localRunnerBindings(text) {
|
|
427
428
|
const locals = /* @__PURE__ */ new Set();
|
|
428
|
-
const decl = new RegExp(String.raw`\{(?<names>[^}]*)\}\s*=\s*await\s+specification\.(?:${CONSTRUCTORS})\s*\(`, "
|
|
429
|
+
const decl = new RegExp(String.raw`\{(?<names>[^}]*)\}\s*=\s*await\s+specification\.(?:${CONSTRUCTORS})\s*\(`, "gu");
|
|
429
430
|
for (const match of text.matchAll(decl)) for (const raw of (match.groups?.names ?? "").split(",")) {
|
|
430
431
|
const part = raw.trim();
|
|
431
|
-
const local = (part.includes(":") ? part.split(":")[1] : part).trim();
|
|
432
|
-
if (local.length > 0 && !NON_RUNNER_BINDINGS.has(part.split(":")[0].trim())) locals.add(local);
|
|
432
|
+
const local = (part.includes(":") ? part.split(":")[1] ?? "" : part).trim();
|
|
433
|
+
if (local.length > 0 && !NON_RUNNER_BINDINGS.has((part.split(":")[0] ?? "").trim())) locals.add(local);
|
|
433
434
|
}
|
|
434
435
|
return locals;
|
|
435
436
|
}
|
|
436
437
|
/** The identifier immediately before a `.seed(`/`.table(` member, if any. */
|
|
437
438
|
function memberObjectIdentifier(text, dotIndex) {
|
|
438
439
|
const before = text.slice(0, dotIndex);
|
|
439
|
-
const match = /(?<name>[A-Za-z_$][\w$]*)\s
|
|
440
|
+
const match = /(?<name>[A-Za-z_$][\w$]*)\s*$/u.exec(before);
|
|
440
441
|
return match === null ? match : match.groups?.name ?? null;
|
|
441
442
|
}
|
|
442
443
|
function checkDatabaseProperty(rootDir) {
|
|
@@ -455,17 +456,18 @@ function checkDatabaseProperty(rootDir) {
|
|
|
455
456
|
const model = target === void 0 ? void 0 : enforced.get(target);
|
|
456
457
|
if (model?.runnerExports.has(binding.name) === true) applicable.add(model);
|
|
457
458
|
}
|
|
458
|
-
|
|
459
|
-
|
|
459
|
+
const [onlyApplicable] = applicable;
|
|
460
|
+
if (applicable.size !== 1 || onlyApplicable === void 0) continue;
|
|
461
|
+
const count = onlyApplicable.sqlDatabaseCount ?? 0;
|
|
460
462
|
const locals = localRunnerBindings(text);
|
|
461
463
|
const suppressed = suppressedLines(readText$1(testFile), "a7");
|
|
462
464
|
const rel = relative(rootDir, testFile);
|
|
463
|
-
for (const match of text.matchAll(/\.(?<verb>seed|table)\s*\(/
|
|
465
|
+
for (const match of text.matchAll(/\.(?<verb>seed|table)\s*\(/gu)) {
|
|
464
466
|
const verb = match.groups?.verb ?? "";
|
|
465
467
|
const object = memberObjectIdentifier(text, match.index);
|
|
466
468
|
if (object !== null && locals.has(object)) continue;
|
|
467
469
|
const args = balancedParens(text, text.indexOf("(", match.index));
|
|
468
|
-
const hasDatabase = /\bdatabase\s
|
|
470
|
+
const hasDatabase = /\bdatabase\s*:/u.test(args);
|
|
469
471
|
const line = lineAt(text, match.index);
|
|
470
472
|
if (suppressed.has(line)) continue;
|
|
471
473
|
if (count >= 2 && !hasDatabase) violations.push({
|
|
@@ -584,7 +586,7 @@ function checkPoolFixtures(rootDir) {
|
|
|
584
586
|
return violations;
|
|
585
587
|
}
|
|
586
588
|
/** A `.fixture('…')` call with a plain string argument. */
|
|
587
|
-
const FIXTURE_CALL = /\.fixture\(\s*(?<quote>['"`])(?<path>(?:[^'"`\\]|\\.)*)\k<quote>/
|
|
589
|
+
const FIXTURE_CALL = /\.fixture\(\s*(?<quote>['"`])(?<path>(?:[^'"`\\]|\\.)*)\k<quote>/gu;
|
|
588
590
|
/**
|
|
589
591
|
* Every fixture path a spec file names, with the line it names it on. A
|
|
590
592
|
* `<case>.spec.yaml` is read through the runner's own parser; a `.ts` file
|
|
@@ -620,7 +622,7 @@ function listReferrers(specsRoot, pool) {
|
|
|
620
622
|
/** The pool entry a `$FIXTURES/…` path names — its FIRST segment. */
|
|
621
623
|
function poolEntryOf(path) {
|
|
622
624
|
if (!path.startsWith("$FIXTURES/")) return;
|
|
623
|
-
const entry = path.slice(10).split("/")[0];
|
|
625
|
+
const entry = path.slice(10).split("/")[0] ?? "";
|
|
624
626
|
return entry.length > 0 ? entry : void 0;
|
|
625
627
|
}
|
|
626
628
|
/** Pool entry → the spec directories that reference it, each with its referrers. */
|
|
@@ -647,8 +649,9 @@ function misplacedPoolFixtures(rootDir) {
|
|
|
647
649
|
for (const pool of findPools(rootDir)) {
|
|
648
650
|
const usage = poolUsage(dirname(pool), pool);
|
|
649
651
|
for (const [entry, leaves] of usage) {
|
|
650
|
-
|
|
651
|
-
|
|
652
|
+
const [only] = leaves;
|
|
653
|
+
if (leaves.size !== 1 || only === void 0 || !isDir(join(pool, entry))) continue;
|
|
654
|
+
const [leaf, referrers] = only;
|
|
652
655
|
misplaced.push({
|
|
653
656
|
entry,
|
|
654
657
|
leaf,
|
|
@@ -869,7 +872,7 @@ function checkBlockScalars(file, lineOf) {
|
|
|
869
872
|
}
|
|
870
873
|
return findings;
|
|
871
874
|
}
|
|
872
|
-
const KEBAB = /^[a-z0-9]+(?:-[a-z0-9]+)
|
|
875
|
+
const KEBAB = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
873
876
|
/** Words the `.spec.yaml` suffix already says — a case name repeating one says nothing. */
|
|
874
877
|
const REDUNDANT_WORDS = /* @__PURE__ */ new Set([
|
|
875
878
|
"cli",
|
|
@@ -898,7 +901,7 @@ function checkFileName(rel) {
|
|
|
898
901
|
}
|
|
899
902
|
const MAX_DESCRIPTION = 100;
|
|
900
903
|
/** A first word that is an identifier, not prose — exempt from the lowercase rule. */
|
|
901
|
-
const SYMBOL_WORD = /^[A-Z0-9_]
|
|
904
|
+
const SYMBOL_WORD = /^[A-Z0-9_]+$/u;
|
|
902
905
|
/**
|
|
903
906
|
* The description IS the vitest test title, and it is held to the title rules:
|
|
904
907
|
* one lowercase line of prose, no trailing period, short enough to read in a
|
|
@@ -915,26 +918,26 @@ function checkDescription(document) {
|
|
|
915
918
|
}
|
|
916
919
|
if (text.length > MAX_DESCRIPTION) findings.push(at(`description: is ${text.length} characters — keep it under ${MAX_DESCRIPTION}`));
|
|
917
920
|
if (text.endsWith(".")) findings.push(at("description: ends with a period — a title is a fragment, not a sentence"));
|
|
918
|
-
const first = text.split(/\s+/)[0] ?? "";
|
|
919
|
-
if (/^[A-Z]
|
|
921
|
+
const first = text.split(/\s+/u)[0] ?? "";
|
|
922
|
+
if (/^[A-Z]/u.test(text) && !SYMBOL_WORD.test(first)) findings.push(at("description: starts with a capital — a title is lowercase prose"));
|
|
920
923
|
return findings;
|
|
921
924
|
}
|
|
922
925
|
/** The literals the update writer must have failed to tokenise, and their token. */
|
|
923
926
|
const VOLATILE = [
|
|
924
927
|
{
|
|
925
|
-
pattern: /\b(?:127\.0\.0\.1|localhost|0\.0\.0\.0):\d{2,5}\b
|
|
928
|
+
pattern: /\b(?:127\.0\.0\.1|localhost|0\.0\.0\.0):\d{2,5}\b/u,
|
|
926
929
|
token: "{{url}} (or {{port}})"
|
|
927
930
|
},
|
|
928
931
|
{
|
|
929
|
-
pattern: /(?:^|[\s"'(=])(?:\/private)?\/tmp
|
|
932
|
+
pattern: /(?:^|[\s"'(=])(?:\/private)?\/tmp\//u,
|
|
930
933
|
token: "{{workdir}}"
|
|
931
934
|
},
|
|
932
935
|
{
|
|
933
|
-
pattern: /(?:^|[\s"'(=])\/var\/folders
|
|
936
|
+
pattern: /(?:^|[\s"'(=])\/var\/folders\//u,
|
|
934
937
|
token: "{{workdir}}"
|
|
935
938
|
},
|
|
936
939
|
{
|
|
937
|
-
pattern: /(?:^|[\s"'(=])\/(?:Users|home)\/[^\s"'/]
|
|
940
|
+
pattern: /(?:^|[\s"'(=])\/(?:Users|home)\/[^\s"'/]+\//u,
|
|
938
941
|
token: "{{workdir}} or {{path}}"
|
|
939
942
|
}
|
|
940
943
|
];
|
|
@@ -955,8 +958,8 @@ function checkVolatileLiterals(document) {
|
|
|
955
958
|
}
|
|
956
959
|
return findings;
|
|
957
960
|
}
|
|
958
|
-
const ISO8601 = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})
|
|
959
|
-
const UUID = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}
|
|
961
|
+
const ISO8601 = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})/u;
|
|
962
|
+
const UUID = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/u;
|
|
960
963
|
/** Everything the document itself FIXED: what it feeds in, and what its fixtures hold. */
|
|
961
964
|
function fixedData(document, dir) {
|
|
962
965
|
const parts = document.runs.map((run) => run.stdin ?? "");
|
|
@@ -1028,7 +1031,7 @@ function objectKeys(source, from) {
|
|
|
1028
1031
|
let depth = 0;
|
|
1029
1032
|
let quote = "";
|
|
1030
1033
|
for (let index = from; index < source.length; index += 1) {
|
|
1031
|
-
const char = source[index];
|
|
1034
|
+
const char = source[index] ?? "";
|
|
1032
1035
|
if (quote !== "") {
|
|
1033
1036
|
if (char === quote && source[index - 1] !== "\\") quote = "";
|
|
1034
1037
|
continue;
|
|
@@ -1049,10 +1052,11 @@ function objectKeys(source, from) {
|
|
|
1049
1052
|
};
|
|
1050
1053
|
continue;
|
|
1051
1054
|
}
|
|
1052
|
-
if (depth === 1 && /[A-Za-z_$'"]
|
|
1053
|
-
const rest = /^(?<quote>['"]?)(?<name>[A-Za-z_$][\w$]*)\k<quote>\s
|
|
1054
|
-
|
|
1055
|
-
|
|
1055
|
+
if (depth === 1 && /[A-Za-z_$'"]/u.test(char)) {
|
|
1056
|
+
const rest = /^(?<quote>['"]?)(?<name>[A-Za-z_$][\w$]*)\k<quote>\s*:/u.exec(source.slice(index));
|
|
1057
|
+
const name = rest?.groups?.name;
|
|
1058
|
+
if (rest !== null && name !== void 0) {
|
|
1059
|
+
keys.push(name);
|
|
1056
1060
|
index += rest[0].length - 1;
|
|
1057
1061
|
}
|
|
1058
1062
|
}
|
|
@@ -1083,12 +1087,12 @@ function registeredNames(dir) {
|
|
|
1083
1087
|
const serve = /* @__PURE__ */ new Set();
|
|
1084
1088
|
for (const entry of entries) {
|
|
1085
1089
|
const text = readText(join(current, entry));
|
|
1086
|
-
for (const call of text.matchAll(/specification\.cli\s*\(/
|
|
1090
|
+
for (const call of text.matchAll(/specification\.cli\s*\(/gu)) {
|
|
1087
1091
|
const open = text.indexOf("{", call.index + call[0].length);
|
|
1088
1092
|
if (open === -1) continue;
|
|
1089
1093
|
const options = objectKeys(text, open);
|
|
1090
1094
|
for (const [key, into] of [["env", env], ["serve", serve]]) {
|
|
1091
|
-
const marker = new RegExp(String.raw`\b${key}\s*:\s*\{
|
|
1095
|
+
const marker = new RegExp(String.raw`\b${key}\s*:\s*\{`, "u").exec(text.slice(open, options.end));
|
|
1092
1096
|
if (marker === null) continue;
|
|
1093
1097
|
const at = open + marker.index + marker[0].length - 1;
|
|
1094
1098
|
for (const name of objectKeys(text, at).keys) into.add(name);
|
|
@@ -1134,7 +1138,7 @@ function checkSpecConventions(text, rel, path) {
|
|
|
1134
1138
|
}
|
|
1135
1139
|
const lineAt = (node) => {
|
|
1136
1140
|
const range = "range" in node && Array.isArray(node.range) ? node.range : void 0;
|
|
1137
|
-
const key = node
|
|
1141
|
+
const { key } = node;
|
|
1138
1142
|
const target = range ?? (isScalar(key) && Array.isArray(key.range) ? key.range : void 0);
|
|
1139
1143
|
return target === void 0 ? 1 : file.source.lineAt(target[0]);
|
|
1140
1144
|
};
|
|
@@ -1204,7 +1208,7 @@ function fixSpecDocument(text, rel) {
|
|
|
1204
1208
|
const reorder = (map, order) => {
|
|
1205
1209
|
const sorted = [...pairsOf(map).filter((pair) => order.includes(keyName(pair) ?? ""))].sort((a, b) => order.indexOf(keyName(a)) - order.indexOf(keyName(b)));
|
|
1206
1210
|
let cursor = 0;
|
|
1207
|
-
map.items = pairsOf(map).map((pair) => order.includes(keyName(pair) ?? "") ? sorted[cursor++] : pair);
|
|
1211
|
+
map.items = pairsOf(map).map((pair) => (order.includes(keyName(pair) ?? "") ? sorted[cursor++] : pair) ?? pair);
|
|
1208
1212
|
};
|
|
1209
1213
|
const map = documentMap(file);
|
|
1210
1214
|
if (map === null) return null;
|
|
@@ -1252,11 +1256,11 @@ function fixSpecFiles(rootDir) {
|
|
|
1252
1256
|
* It shares TOKEN_KINDS with the runtime matcher so the channels cannot drift.
|
|
1253
1257
|
*/
|
|
1254
1258
|
/** A well-formed token: `{{word}}` / `{{word#ref}}`. */
|
|
1255
|
-
const VALID_TOKEN = /^[A-Za-z][A-Za-z0-9]*(?:#[\w.-]+)
|
|
1259
|
+
const VALID_TOKEN = /^[A-Za-z][A-Za-z0-9]*(?:#[\w.-]+)?$/u;
|
|
1256
1260
|
/** Any `{{ … }}` block (no nested braces) — classified by the scanner below. */
|
|
1257
|
-
const BRACE_BLOCK = /\{\{(?<inner>[^{}]*)\}\}/
|
|
1261
|
+
const BRACE_BLOCK = /\{\{(?<inner>[^{}]*)\}\}/gu;
|
|
1258
1262
|
/** The leading identifier of a brace block, for malformed-ref classification. */
|
|
1259
|
-
const LEADING_WORD = /^(?<kind>[A-Za-z][A-Za-z0-9]*)
|
|
1263
|
+
const LEADING_WORD = /^(?<kind>[A-Za-z][A-Za-z0-9]*)/u;
|
|
1260
1264
|
const KNOWN = new Set(TOKEN_KINDS);
|
|
1261
1265
|
/** Directories whose files carry the token grammar (D4). */
|
|
1262
1266
|
const FIXTURE_DIRS = /* @__PURE__ */ new Set([GROUND_EXPECTED, GROUND_REQUESTS]);
|
|
@@ -1283,7 +1287,7 @@ function findUnknownTokens(text) {
|
|
|
1283
1287
|
for (const [index, lineText] of lines.entries()) for (const match of lineText.matchAll(BRACE_BLOCK)) {
|
|
1284
1288
|
const inner = match.groups?.inner ?? "";
|
|
1285
1289
|
if (VALID_TOKEN.test(inner)) {
|
|
1286
|
-
if (!KNOWN.has(inner.split("#")[0])) violations.push({
|
|
1290
|
+
if (!KNOWN.has(inner.split("#")[0] ?? "")) violations.push({
|
|
1287
1291
|
line: index + 1,
|
|
1288
1292
|
token: match[0]
|
|
1289
1293
|
});
|
|
@@ -1303,7 +1307,7 @@ function findKnownTokens(text) {
|
|
|
1303
1307
|
const lines = text.split("\n");
|
|
1304
1308
|
for (const [index, lineText] of lines.entries()) for (const match of lineText.matchAll(BRACE_BLOCK)) {
|
|
1305
1309
|
const inner = match.groups?.inner ?? "";
|
|
1306
|
-
if (VALID_TOKEN.test(inner) && KNOWN.has(inner.split("#")[0])) found.push({
|
|
1310
|
+
if (VALID_TOKEN.test(inner) && KNOWN.has(inner.split("#")[0] ?? "")) found.push({
|
|
1307
1311
|
line: index + 1,
|
|
1308
1312
|
token: match[0]
|
|
1309
1313
|
});
|
|
@@ -1325,8 +1329,8 @@ function firstLine(text) {
|
|
|
1325
1329
|
for (const line of text.split("\n")) if (line.trim().length > 0) return line.trim();
|
|
1326
1330
|
return "";
|
|
1327
1331
|
}
|
|
1328
|
-
const REQUEST_LINE = /^(?<method>GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS) \/\S
|
|
1329
|
-
const STATUS_LINE = /^HTTP\/\d(?:\.\d)? \d{3}\b
|
|
1332
|
+
const REQUEST_LINE = /^(?<method>GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS) \/\S*/u;
|
|
1333
|
+
const STATUS_LINE = /^HTTP\/\d(?:\.\d)? \d{3}\b/u;
|
|
1330
1334
|
/**
|
|
1331
1335
|
* Check one `<case>.spec.yaml` — D4b's shape through the SAME parser the runner
|
|
1332
1336
|
* uses (one grammar, no drift), then D4's token vocabulary over the STREAMS
|
|
@@ -1386,10 +1390,7 @@ function checkConventionFiles(rootDir) {
|
|
|
1386
1390
|
}
|
|
1387
1391
|
if (entry.name.endsWith(".spec.yaml")) {
|
|
1388
1392
|
const text = decodeText(path);
|
|
1389
|
-
if (text !== null)
|
|
1390
|
-
violations.push(...checkSpecFile(text, rel));
|
|
1391
|
-
violations.push(...checkSpecConventions(text, rel, path));
|
|
1392
|
-
}
|
|
1393
|
+
if (text !== null) violations.push(...checkSpecFile(text, rel), ...checkSpecConventions(text, rel, path));
|
|
1393
1394
|
continue;
|
|
1394
1395
|
}
|
|
1395
1396
|
if (inside === null) continue;
|