-
-
Notifications
You must be signed in to change notification settings - Fork 212
Expand file tree
/
Copy pathtemplate-lint-disable.js
More file actions
153 lines (135 loc) · 5.42 KB
/
template-lint-disable.js
File metadata and controls
153 lines (135 loc) · 5.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
'use strict';
const noop = require('ember-eslint-parser/noop');
/**
* Regex patterns for template-lint-disable mustache comments.
* Two separate patterns to avoid polynomial backtracking (ReDoS):
* {{! template-lint-disable rule1 rule2 }}
* {{!-- template-lint-disable rule1 rule2 --}}
*/
// Lookahead (?=\s|…) ensures we match exactly "template-lint-disable"
// and not "template-lint-disable-next-line" or "template-lint-disable-tree".
// \s+ after ! — required because {{!text}} is valid Handlebars (comment with no space)
const MUSTACHE_COMMENT_REGEX = /{{!\s+template-lint-disable(?=\s|}})([\s\w,/@-]*)}}/g;
// \s* after -- — the dashes already delimit, so {{!--template-lint-disable--}} is fine
const MUSTACHE_BLOCK_COMMENT_REGEX = /{{!--\s*template-lint-disable(?=\s|--)([\s\w,/@-]*)--}}/g;
const GJS_GTS_EXT = /\.(gjs|gts)$/;
// Store disable directives per file
const fileDisableDirectives = new Map();
/**
* Parse template-lint-disable comments from source text and store
* which lines/rules should be suppressed.
*
* template-lint-disable means "disable next line" (like eslint-disable-next-line).
*
* NOTE: In ember-template-lint, `template-lint-disable` disables from that point
* for the rest of the scope (until `template-lint-enable`). This implementation
* intentionally uses "next line only" semantics to match ESLint conventions.
* `template-lint-enable` is not supported and will be silently ignored.
* `template-lint-disable-next-line` and `template-lint-disable-tree` are also
* not matched — only the exact `template-lint-disable` directive is recognized.
*
* Errors on the comment line itself are also suppressed (similar to
* eslint-disable-line), and errors on the next line are suppressed
* (similar to eslint-disable-next-line). This dual behavior exists because
* template AST TextNodes can start on the same line as the comment.
*
* CAVEAT: The processor runs on the entire file text, not just template regions.
* A JS string literal containing `{{! template-lint-disable }}` in a gjs file
* would match and suppress errors on the same/next line. This is unlikely in
* practice but impossible to fix without region-aware parsing.
*/
function collectMatches(line, lineIndex, directives) {
for (const regex of [MUSTACHE_COMMENT_REGEX, MUSTACHE_BLOCK_COMMENT_REGEX]) {
regex.lastIndex = 0;
let match;
while ((match = regex.exec(line)) !== null) {
const rulesPart = (match[1] || '').trim();
const rules = rulesPart ? rulesPart.split(/[\s,]+/).filter(Boolean) : []; // empty = disable all
// Comment is on line lineIndex+1 (1-indexed), next line is lineIndex+2.
// In template ASTs, TextNodes can start on the same line as the comment
// (e.g. the newline after {{! template-lint-disable }} is part of the
// following TextNode), so we suppress both the comment line and the next.
directives.push({
commentLine: lineIndex + 1,
nextLine: lineIndex + 2,
rules,
});
}
}
}
function parseDisableDirectives(text) {
const directives = [];
const lines = text.split('\n');
for (const [i, line] of lines.entries()) {
collectMatches(line, i, directives);
}
return directives;
}
/**
* Map a rule name from template-lint format to eslint-plugin-ember format.
* Accepts three forms:
* "no-bare-strings" -> "ember/template-no-bare-strings"
* "template-no-bare-strings" -> "ember/template-no-bare-strings"
* "ember/template-no-bare-strings" -> exact match
*/
function matchesRule(ruleId, disableRuleName) {
if (ruleId === disableRuleName) {
return true;
}
// e.g. "template-no-bare-strings" -> "ember/template-no-bare-strings"
if (ruleId === `ember/${disableRuleName}`) {
return true;
}
// e.g. "no-bare-strings" -> "ember/template-no-bare-strings"
if (ruleId === `ember/template-${disableRuleName}`) {
return true;
}
return false;
}
function shouldSuppressMessage(message, directives) {
for (const directive of directives) {
if (message.line !== directive.commentLine && message.line !== directive.nextLine) {
continue;
}
// No rules specified = suppress all
if (directive.rules.length === 0) {
return true;
}
// Check if any specified rule matches this message's rule
if (directive.rules.some((rule) => matchesRule(message.ruleId, rule))) {
return true;
}
}
return false;
}
module.exports = {
preprocess(text, fileName) {
if (!text.includes('template-lint-disable')) {
fileDisableDirectives.delete(fileName);
return [text];
}
const directives = parseDisableDirectives(text);
if (directives.length > 0) {
fileDisableDirectives.set(fileName, directives);
} else {
fileDisableDirectives.delete(fileName);
}
// Return text as-is (single code block)
return [text];
},
postprocess(messages, fileName) {
// Only run noop's postprocess for gjs/gts files — it appends gjs/gts setup
// instructions on parse failures, which corrupts hbs error messages since
// the hbs parser never calls registerParsedFile.
const msgs = GJS_GTS_EXT.test(fileName)
? noop.postprocess(messages, fileName)
: messages.flat();
const directives = fileDisableDirectives.get(fileName);
if (!directives) {
return msgs;
}
fileDisableDirectives.delete(fileName);
return msgs.filter((message) => !shouldSuppressMessage(message, directives));
},
supportsAutofix: true,
};