Skip to content

Commit 3cb9223

Browse files
committed
Add types to TextMate gulp step
1 parent 9e2b16a commit 3cb9223

3 files changed

Lines changed: 158 additions & 41 deletions

File tree

extensions/ql-vscode/.eslintrc.js

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -121,15 +121,6 @@ module.exports = {
121121
},
122122
},
123123
},
124-
{
125-
files: ["test/**/*"],
126-
parserOptions: {
127-
project: resolve(__dirname, "test/tsconfig.json"),
128-
},
129-
env: {
130-
jest: true,
131-
},
132-
},
133124
{
134125
files: ["test/vscode-tests/**/*"],
135126
parserOptions: {
@@ -156,6 +147,18 @@ module.exports = {
156147
],
157148
},
158149
},
150+
{
151+
files: ["test/**/*"],
152+
parserOptions: {
153+
project: resolve(__dirname, "test/tsconfig.json"),
154+
},
155+
env: {
156+
jest: true,
157+
},
158+
rules: {
159+
"@typescript-eslint/no-explicit-any": "off",
160+
},
161+
},
159162
{
160163
files: [
161164
".eslintrc.js",
@@ -188,11 +191,5 @@ module.exports = {
188191
"import/no-namespace": ["error", { ignore: ["react"] }],
189192
},
190193
},
191-
{
192-
files: ["test/**/*", "gulpfile.ts/**/*"],
193-
rules: {
194-
"@typescript-eslint/no-explicit-any": "off",
195-
},
196-
},
197194
],
198195
};
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/**
2+
* A subset of the standard TextMate grammar that is used by our transformation
3+
* step. For a full JSON schema, see:
4+
* https://github.com/martinring/tmlanguage/blob/478ad124a21933cd4b0b65f1ee7ee18ee1f87473/tmlanguage.json
5+
*/
6+
export interface TextmateGrammar {
7+
patterns: Pattern[];
8+
repository?: Record<string, Pattern>;
9+
}
10+
11+
/**
12+
* The extended TextMate grammar as used by our transformation step. This is a superset of the
13+
* standard TextMate grammar, and includes additional fields that are used by our transformation
14+
* step.
15+
*
16+
* Any comment of the form `(?#ref-id)` in a `match`, `begin`, or `end` property will be replaced
17+
* with the match text of the rule named "ref-id". If the rule named "ref-id" consists of just a
18+
* `patterns` property with a list of `include` directives, the replacement pattern is the
19+
* disjunction of the match patterns of all of the included rules.
20+
*/
21+
export interface ExtendedTextmateGrammar<MatchType = string> {
22+
/**
23+
* This represents the set of regular expression options to apply to all regular
24+
* expressions throughout the file.
25+
*/
26+
regexOptions?: string;
27+
/**
28+
* This element defines a map of macro names to replacement text. When a `match`, `begin`, or
29+
* `end` property has a value that is a single-key map, the value is replaced with the value of the
30+
* macro named by the key, with any use of `(?#)` in the macro text replaced with the text of the
31+
* value of the key, surrounded by a non-capturing group (`(?:)`). For example:
32+
*
33+
* The `beginPattern` and `endPattern` Properties
34+
* A rule can have a `beginPattern` or `endPattern` property whose value is a reference to another
35+
* rule (e.g. `#other-rule`). The `beginPattern` property is replaced as follows:
36+
*
37+
* my-rule:
38+
* beginPattern: '#other-rule'
39+
*
40+
* would be transformed to
41+
*
42+
* my-rule:
43+
* begin: '(?#other-rule)'
44+
* beginCaptures:
45+
* '0':
46+
* patterns:
47+
* - include: '#other-rule'
48+
*
49+
* An `endPattern` property is transformed similary.
50+
*
51+
* macros:
52+
* repeat: '(?#)*'
53+
* repository:
54+
* multi-letter:
55+
* match:
56+
* repeat: '[A-Za-z]'
57+
* name: scope.multi-letter
58+
*
59+
* would be transformed to
60+
*
61+
* repository:
62+
* multi-letter:
63+
* match: '(?:[A-Za-z])*'
64+
* name: scope.multi-letter
65+
*/
66+
macros?: Record<string, string>;
67+
68+
patterns: Array<Pattern<MatchType>>;
69+
repository?: Record<string, Pattern<MatchType>>;
70+
}
71+
72+
export interface Pattern<MatchType = string> {
73+
include?: string;
74+
match?: MatchType;
75+
begin?: MatchType;
76+
end?: MatchType;
77+
while?: MatchType;
78+
captures?: Record<string, PatternCapture>;
79+
beginCaptures?: Record<string, PatternCapture>;
80+
endCaptures?: Record<string, PatternCapture>;
81+
patterns?: Array<Pattern<MatchType>>;
82+
beginPattern?: string;
83+
endPattern?: string;
84+
}
85+
86+
export interface PatternCapture {
87+
name?: string;
88+
patterns?: Pattern[];
89+
}
90+
91+
export type ExtendedMatchType = string | Record<string, string>;

extensions/ql-vscode/gulpfile.ts/textmate.ts

Lines changed: 55 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ import { load } from "js-yaml";
33
import { obj } from "through2";
44
import PluginError from "plugin-error";
55
import type Vinyl from "vinyl";
6+
import type {
7+
ExtendedMatchType,
8+
ExtendedTextmateGrammar,
9+
Pattern,
10+
TextmateGrammar,
11+
} from "./textmate-grammar";
612

713
/**
814
* Replaces all rule references with the match pattern of the referenced rule.
@@ -34,7 +40,9 @@ function replaceReferencesWithStrings(
3440
* @param yaml The root of the YAML document.
3541
* @returns A map from macro name to replacement text.
3642
*/
37-
function gatherMacros(yaml: any): Map<string, string> {
43+
function gatherMacros<T>(
44+
yaml: ExtendedTextmateGrammar<T>,
45+
): Map<string, string> {
3846
const macros = new Map<string, string>();
3947
for (const key in yaml.macros) {
4048
macros.set(key, yaml.macros[key]);
@@ -51,7 +59,7 @@ function gatherMacros(yaml: any): Map<string, string> {
5159
* @returns The match text for the rule. This is either the value of the rule's `match` property,
5260
* or the disjunction of the match text of all of the other rules `include`d by this rule.
5361
*/
54-
function getNodeMatchText(rule: any): string {
62+
function getNodeMatchText(rule: Pattern): string {
5563
if (rule.match !== undefined) {
5664
// For a match string, just use that string as the replacement.
5765
return rule.match;
@@ -78,7 +86,7 @@ function getNodeMatchText(rule: any): string {
7886
* @returns A map whose keys are the names of rules, and whose values are the corresponding match
7987
* text of each rule.
8088
*/
81-
function gatherMatchTextForRules(yaml: any): Map<string, string> {
89+
function gatherMatchTextForRules(yaml: TextmateGrammar): Map<string, string> {
8290
const replacements = new Map<string, string>();
8391
for (const key in yaml.repository) {
8492
const node = yaml.repository[key];
@@ -94,9 +102,14 @@ function gatherMatchTextForRules(yaml: any): Map<string, string> {
94102
* @param yaml The root of the YAML document.
95103
* @param action Callback to invoke on each rule.
96104
*/
97-
function visitAllRulesInFile(yaml: any, action: (rule: any) => void) {
105+
function visitAllRulesInFile<T>(
106+
yaml: ExtendedTextmateGrammar<T>,
107+
action: (rule: Pattern<T>) => void,
108+
) {
98109
visitAllRulesInRuleMap(yaml.patterns, action);
99-
visitAllRulesInRuleMap(yaml.repository, action);
110+
if (yaml.repository) {
111+
visitAllRulesInRuleMap(Object.values(yaml.repository), action);
112+
}
100113
}
101114

102115
/**
@@ -107,9 +120,11 @@ function visitAllRulesInFile(yaml: any, action: (rule: any) => void) {
107120
* @param ruleMap The map or array of rules to visit.
108121
* @param action Callback to invoke on each rule.
109122
*/
110-
function visitAllRulesInRuleMap(ruleMap: any, action: (rule: any) => void) {
111-
for (const key in ruleMap) {
112-
const rule = ruleMap[key];
123+
function visitAllRulesInRuleMap<T>(
124+
ruleMap: Array<Pattern<T>>,
125+
action: (rule: Pattern<T>) => void,
126+
) {
127+
for (const rule of ruleMap) {
113128
if (typeof rule === "object") {
114129
action(rule);
115130
if (rule.patterns !== undefined) {
@@ -125,16 +140,22 @@ function visitAllRulesInRuleMap(ruleMap: any, action: (rule: any) => void) {
125140
* @param rule The rule whose matches are to be transformed.
126141
* @param action The transformation to make on each match pattern.
127142
*/
128-
function visitAllMatchesInRule(rule: any, action: (match: any) => any) {
143+
function visitAllMatchesInRule<T>(rule: Pattern<T>, action: (match: T) => T) {
129144
for (const key in rule) {
130145
switch (key) {
131146
case "begin":
132147
case "end":
133148
case "match":
134-
case "while":
135-
rule[key] = action(rule[key]);
136-
break;
149+
case "while": {
150+
const ruleElement = rule[key];
137151

152+
if (!ruleElement) {
153+
continue;
154+
}
155+
156+
rule[key] = action(ruleElement);
157+
break;
158+
}
138159
default:
139160
break;
140161
}
@@ -148,14 +169,17 @@ function visitAllMatchesInRule(rule: any, action: (match: any) => any) {
148169
* @param rule Rule to be transformed.
149170
* @param key Base key of the property to be transformed.
150171
*/
151-
function expandPatternMatchProperties(rule: any, key: "begin" | "end") {
152-
const patternKey = `${key}Pattern`;
153-
const capturesKey = `${key}Captures`;
172+
function expandPatternMatchProperties<T>(
173+
rule: Pattern<T>,
174+
key: "begin" | "end",
175+
) {
176+
const patternKey = `${key}Pattern` as const;
177+
const capturesKey = `${key}Captures` as const;
154178
const pattern = rule[patternKey];
155179
if (pattern !== undefined) {
156180
const patterns: string[] = Array.isArray(pattern) ? pattern : [pattern];
157-
rule[key] = patterns.map((p) => `((?${p}))`).join("|");
158-
const captures: { [index: string]: any } = {};
181+
rule[key] = patterns.map((p) => `((?${p}))`).join("|") as T;
182+
const captures: Pattern["captures"] = {};
159183
for (const patternIndex in patterns) {
160184
captures[(Number(patternIndex) + 1).toString()] = {
161185
patterns: [
@@ -175,7 +199,7 @@ function expandPatternMatchProperties(rule: any, key: "begin" | "end") {
175199
*
176200
* @param yaml The root of the YAML document.
177201
*/
178-
function transformFile(yaml: any) {
202+
function transformFile(yaml: ExtendedTextmateGrammar<ExtendedMatchType>) {
179203
const macros = gatherMacros(yaml);
180204
visitAllRulesInFile(yaml, (rule) => {
181205
expandPatternMatchProperties(rule, "begin");
@@ -198,24 +222,29 @@ function transformFile(yaml: any) {
198222

199223
yaml.macros = undefined;
200224

201-
const replacements = gatherMatchTextForRules(yaml);
225+
// We have removed all object match properties, so we don't have an extended match type anymore.
226+
const macrolessYaml = yaml as ExtendedTextmateGrammar;
227+
228+
const replacements = gatherMatchTextForRules(macrolessYaml);
202229
// Expand references in matches.
203-
visitAllRulesInFile(yaml, (rule) => {
230+
visitAllRulesInFile(macrolessYaml, (rule) => {
204231
visitAllMatchesInRule(rule, (match) => {
205232
return replaceReferencesWithStrings(match, replacements);
206233
});
207234
});
208235

209-
if (yaml.regexOptions !== undefined) {
210-
const regexOptions = `(?${yaml.regexOptions})`;
211-
visitAllRulesInFile(yaml, (rule) => {
236+
if (macrolessYaml.regexOptions !== undefined) {
237+
const regexOptions = `(?${macrolessYaml.regexOptions})`;
238+
visitAllRulesInFile(macrolessYaml, (rule) => {
212239
visitAllMatchesInRule(rule, (match) => {
213240
return regexOptions + match;
214241
});
215242
});
216243

217-
yaml.regexOptions = undefined;
244+
macrolessYaml.regexOptions = undefined;
218245
}
246+
247+
return macrolessYaml;
219248
}
220249

221250
export function transpileTextMateGrammar() {
@@ -230,8 +259,8 @@ export function transpileTextMateGrammar() {
230259
} else if (file.isBuffer()) {
231260
const buf: Buffer = file.contents;
232261
const yamlText: string = buf.toString("utf8");
233-
const jsonData: any = load(yamlText);
234-
transformFile(jsonData);
262+
const yamlData = load(yamlText) as TextmateGrammar;
263+
const jsonData = transformFile(yamlData);
235264

236265
file.contents = Buffer.from(JSON.stringify(jsonData, null, 2), "utf8");
237266
file.extname = ".json";

0 commit comments

Comments
 (0)