Skip to content

Commit d2461f2

Browse files
authored
Improve assertScopes, add some regex tests (#25)
- assertScopes now supports asserting scopes over a whole range, and correctly tracks multiplicity of scopes - Added a few regex tests based on grammar-test.swift - Upgraded node-test-github-reporter for nearform/node-test-github-reporter#287
1 parent 7750f96 commit d2461f2

File tree

9 files changed

+358
-72
lines changed

9 files changed

+358
-72
lines changed

.vscode/tasks.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"version": "2.0.0",
3+
"tasks": [
4+
{
5+
"type": "npm",
6+
"script": "test",
7+
"group": {
8+
"kind": "test",
9+
"isDefault": true
10+
},
11+
"problemMatcher": [
12+
{
13+
"pattern": {
14+
"regexp": "^test at ([^:]+):(\\d+):(\\d+)$",
15+
"file": 1,
16+
"line": 2,
17+
"column": 3
18+
}
19+
}
20+
],
21+
"label": "npm: test",
22+
"detail": "yarn node --test test/*.test.*"
23+
}
24+
]
25+
}

.yarn/patches/node-test-github-reporter.patch

Lines changed: 0 additions & 11 deletions
This file was deleted.

eslint.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export default tseslint.config(
1717
foxglove.configs.typescript,
1818
{
1919
rules: {
20+
"no-warning-comments": "off",
21+
2022
// prettier plugin currently produces some pnp-related import error
2123
"prettier/prettier": "off",
2224
},

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"@types/plist": "3.0.4",
2727
"commander": "11.1.0",
2828
"eslint": "9.27.0",
29-
"node-test-github-reporter": "patch:node-test-github-reporter@npm%3A1.3.0#~/.yarn/patches/node-test-github-reporter.patch",
29+
"node-test-github-reporter": "1.3.1",
3030
"plist": "3.1.0",
3131
"prettier": "3.5.3",
3232
"shiki": "3.4.2",

test/assert-scopes.test.ts

Lines changed: 89 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ const assertScopes = await createAssertScopes({
1010
patterns: [
1111
{ match: "foo", name: "example.foo" },
1212
{ match: "bar", name: "example.bar" },
13+
{
14+
begin: "begin",
15+
end: "end",
16+
name: "example.range",
17+
patterns: [{ include: "$self" }],
18+
},
1319
],
1420
});
1521

@@ -21,24 +27,77 @@ await test("assertions", () => {
2127
{ message: "Expected a source line before assertion" },
2228
);
2329

30+
assertScopes(
31+
//
32+
$`foo bar`,
33+
_`~~~ example.foo`,
34+
_` ~~~ example.bar`,
35+
$`foo bar`,
36+
_`~~~ example.foo`,
37+
_` ~~~ example.bar`,
38+
);
39+
2440
assert.throws(
2541
() => {
2642
assertScopes(
2743
//
2844
$`foo bar`,
45+
_` ~~~ example.bar`,
2946
_`~~~ example.foo`,
3047
);
3148
},
32-
{ message: "Not enough assertions" },
49+
{ message: "Assertions should be sorted by start index" },
3350
);
3451

35-
assert.throws(() => {
36-
assertScopes(
37-
//
38-
$`foo bar`,
39-
_` ~~~ example.bar`,
40-
);
41-
}, /Skipped token should have no scopes/);
52+
assert.throws(
53+
() => {
54+
assertScopes(
55+
//
56+
$`foo`,
57+
_`~~ example.foo`,
58+
);
59+
},
60+
{
61+
message:
62+
"Partial assertions are not allowed (asserting 0-2 of 0-3: source.test, example.foo)",
63+
},
64+
);
65+
66+
assert.throws(
67+
() => {
68+
assertScopes(
69+
//
70+
$`foo`,
71+
_` ~~ example.foo`,
72+
);
73+
},
74+
{
75+
message:
76+
"Partial assertions are not allowed (asserting 1-3 of 0-3: source.test, example.foo)",
77+
},
78+
);
79+
80+
assert.throws(
81+
() => {
82+
assertScopes(
83+
//
84+
$`foo bar`,
85+
_`~~~ example.foo`,
86+
);
87+
},
88+
{ message: /Missing assertions for 4-7: source\.test, example\.bar/ },
89+
);
90+
91+
assert.throws(
92+
() => {
93+
assertScopes(
94+
//
95+
$`foo bar`,
96+
_` ~~~ example.bar`,
97+
);
98+
},
99+
{ message: /Missing assertions for 0-3: source\.test, example\.foo/ },
100+
);
42101

43102
assert.throws(
44103
() => {
@@ -48,6 +107,27 @@ await test("assertions", () => {
48107
_`~~~~~~ wrong.scope`,
49108
);
50109
},
51-
{ message: "No token found matching assertion (0-6: wrong.scope)" },
110+
{ message: "Token does not match wrong.scope (0-5: source.test)" },
52111
);
112+
113+
assertScopes(
114+
//
115+
$`begin foo end`,
116+
_`~~~~~~~~~~~~~~ example.range`,
117+
_` ~~~ example.foo`,
118+
);
119+
120+
assertScopes(
121+
$`begin begin end end`,
122+
_`~~~~~~~~~~~~~~~~~~~ example.range`,
123+
_` ~~~~~~~~~ example.range`,
124+
);
125+
126+
assert.throws(() => {
127+
assertScopes(
128+
//
129+
$`begin begin end end`,
130+
_`~~~~~~~~~~~~~~~~~~~ example.range`,
131+
);
132+
}, "Incorrect assertions for 6-11: source.test, example.range, example.range");
53133
});

test/assert-scopes.ts

Lines changed: 77 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,54 +3,97 @@ import assert from "node:assert/strict";
33
import { createHighlighter } from "shiki";
44
import type { Grammar, LanguageRegistration } from "shiki";
55

6+
function formatToken(token: IToken) {
7+
return `${token.startIndex}-${token.endIndex}: ${token.scopes.join(", ")}`;
8+
}
9+
610
function assertScopes(
711
grammar: Grammar,
812
rootScopeName: string,
913
...items: (string | ScopeAssertion)[]
1014
) {
11-
function stripRootScope(scopes: string[]): string[] {
12-
if (scopes[0] === rootScopeName) {
13-
return scopes.slice(1);
14-
}
15-
return scopes;
16-
}
17-
15+
let currentLine: string | undefined;
16+
let currentState: ReturnType<Grammar["tokenizeLine"]> | undefined;
17+
let assertedScopesByTokenIdx: string[][] = [];
1818
let currentAssertion: ScopeAssertion | undefined;
19+
const checkMissingAssertions = () => {
20+
if (!currentState || currentLine == undefined) {
21+
return;
22+
}
23+
const noAssertions = assertedScopesByTokenIdx.every((scopes) => scopes.length === 0);
24+
if (noAssertions) {
25+
assert.fail(
26+
"No assertions provided. Suggested assertions:\n" +
27+
currentState.tokens
28+
.flatMap((token) => {
29+
const scopes = token.scopes.filter((scope) => scope !== rootScopeName);
30+
if (scopes.length === 0) {
31+
return [];
32+
}
33+
return [
34+
" _`" +
35+
" ".repeat(token.startIndex) +
36+
"~".repeat(token.endIndex - token.startIndex) +
37+
" ".repeat(currentLine!.length - token.endIndex + 1) +
38+
scopes.join(", ") +
39+
"`,",
40+
];
41+
})
42+
.join("\n"),
43+
);
44+
}
45+
currentState.tokens.forEach((token, i) => {
46+
const asserted = assertedScopesByTokenIdx[i]!;
47+
assert.deepEqual(
48+
asserted.toSorted(),
49+
token.scopes.filter((scope) => scope !== rootScopeName).toSorted(),
50+
`${asserted.length === 0 ? "Missing" : "Incorrect"} assertions for ${formatToken(token)}`,
51+
);
52+
});
53+
};
1954
try {
20-
let state: ReturnType<Grammar["tokenizeLine"]> | undefined;
21-
let tokenIndex = 0;
2255
for (const item of items) {
2356
if (typeof item === "string") {
24-
if (state && tokenIndex < state.tokens.length) {
25-
assert.fail("Not enough assertions");
26-
}
27-
state = grammar.tokenizeLine(item, state?.ruleStack ?? null);
28-
tokenIndex = 0;
57+
checkMissingAssertions();
58+
currentLine = item;
59+
currentState = grammar.tokenizeLine(currentLine, currentState?.ruleStack ?? null);
60+
assertedScopesByTokenIdx = Array.from(currentState.tokens, () => []);
61+
currentAssertion = undefined;
2962
continue;
3063
}
64+
65+
const prevAssertion = currentAssertion;
3166
currentAssertion = item;
32-
if (!state) {
33-
assert.fail("Expected a source line before assertion");
67+
if (prevAssertion) {
68+
assert(
69+
item.startIndex >= prevAssertion.startIndex,
70+
"Assertions should be sorted by start index",
71+
);
3472
}
35-
while (
36-
tokenIndex < state.tokens.length &&
37-
(state.tokens[tokenIndex]!.startIndex !== item.startIndex ||
38-
state.tokens[tokenIndex]!.endIndex !== item.endIndex)
39-
) {
40-
const token: IToken = state.tokens[tokenIndex]!;
41-
assert.deepEqual(stripRootScope(token.scopes), [], "Skipped token should have no scopes");
42-
++tokenIndex;
73+
if (!currentState || currentLine == undefined) {
74+
assert.fail("Expected a source line before assertion");
4375
}
44-
assert(
45-
tokenIndex < state.tokens.length,
46-
`No token found matching assertion (${item.startIndex}-${item.endIndex}: ${item.scopes.join(", ")})`,
47-
);
48-
const token = state.tokens[tokenIndex++]!;
49-
assert.deepEqual(stripRootScope(token.scopes), item.scopes);
50-
}
51-
if (state && tokenIndex < state.tokens.length) {
52-
assert.fail("Not enough assertions");
76+
77+
currentState.tokens.forEach((token, i) => {
78+
if (token.endIndex <= item.startIndex || token.startIndex >= item.endIndex) {
79+
return;
80+
}
81+
if (token.startIndex < item.startIndex || token.endIndex > item.endIndex) {
82+
assert.fail(
83+
`Partial assertions are not allowed (asserting ${item.startIndex}-${item.endIndex} of ${formatToken(token)})`,
84+
);
85+
}
86+
const asserted = assertedScopesByTokenIdx[i]!;
87+
for (const scope of item.scopes) {
88+
assert(
89+
token.scopes.includes(scope),
90+
`Token does not match ${scope} (${formatToken(token)})`,
91+
);
92+
asserted.push(scope);
93+
}
94+
});
5395
}
96+
checkMissingAssertions();
5497
} catch (err) {
5598
if (err instanceof assert.AssertionError) {
5699
const newStack = currentAssertion?.stack;
@@ -87,7 +130,7 @@ class ScopeAssertion {
87130
}
88131

89132
export function $(strings: TemplateStringsArray): string {
90-
return strings[0]!;
133+
return strings.raw[0]!;
91134
}
92135

93136
export function _(strings: TemplateStringsArray): ScopeAssertion {

test/index.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,26 @@ await test("basic", () => {
1313
_` ~~~~~~ support.type.swift`,
1414
);
1515
});
16+
17+
await test("import", () => {
18+
assertScopes(
19+
$`import Foo // whitespace ok`,
20+
_`~~~~~~~~~~~~~ meta.import.swift`,
21+
_`~~~~~~ keyword.control.import.swift`,
22+
_` ~~~ entity.name.type.swift`,
23+
_` ~~~~~~~~~~~~~~~~ comment.line.double-slash.swift`,
24+
_` ~~ punctuation.definition.comment.swift`,
25+
);
26+
27+
assertScopes(
28+
$`import func Control.Monad.>>=`,
29+
_`~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ meta.import.swift`,
30+
_`~~~~~~ keyword.control.import.swift`,
31+
_` ~~~~ storage.modifier.swift`,
32+
_` ~~~~~~~ entity.name.type.swift`,
33+
_` ~ punctuation.separator.import.swift`,
34+
_` ~~~~~ entity.name.type.swift`,
35+
_` ~ punctuation.separator.import.swift`,
36+
_` ~~~ entity.name.type.swift`,
37+
);
38+
});

0 commit comments

Comments
 (0)