UNPKG

13.3 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.DefaultLineSwitchParser = exports.LineSwitchFilterFactory = exports.LINE_SWITCH_REGEX = void 0;
4const tslib_1 = require("tslib");
5const inversify_1 = require("inversify");
6const ts = require("typescript");
7const tsutils_1 = require("tsutils");
8const ymir_1 = require("@fimbul/ymir");
9exports.LINE_SWITCH_REGEX = /^ *wotan-(enable|disable)((?:-next)?-line)?( +(?:(?:[\w-]+\/)*[\w-]+ *, *)*(?:[\w-]+\/)*[\w-]+)? *$/;
10let LineSwitchFilterFactory = class LineSwitchFilterFactory {
11 constructor(parser) {
12 this.parser = parser;
13 }
14 create(context) {
15 const { disables, switches } = this.parseLineSwitches(context);
16 return new Filter(disables, switches, context.sourceFile);
17 }
18 getDisabledRanges(context) {
19 // remove internal `switch` property from ranges
20 return new Map(Array.from(this.parseLineSwitches(context).disables, (entry) => [
21 entry[0],
22 entry[1].map((range) => ({ pos: range.pos, end: range.end })),
23 ]));
24 }
25 parseLineSwitches(context) {
26 var _a, _b;
27 const { sourceFile, ruleNames } = context;
28 let wrappedAst;
29 const raw = this.parser.parse({
30 sourceFile,
31 getCommentAtPosition(pos) {
32 const wrap = tsutils_1.getWrappedNodeAtPosition(wrappedAst !== null && wrappedAst !== void 0 ? wrappedAst : (wrappedAst = context.getWrappedAst()), pos);
33 if (wrap === undefined)
34 return;
35 return tsutils_1.getCommentAtPosition(sourceFile, pos, wrap.node);
36 },
37 });
38 const lineSwitches = [];
39 const result = new Map();
40 for (const rawLineSwitch of raw) {
41 const lineSwitch = {
42 location: rawLineSwitch.location,
43 enable: rawLineSwitch.enable,
44 rules: [],
45 outOfRange: rawLineSwitch.end <= 0 || rawLineSwitch.pos > sourceFile.end,
46 };
47 lineSwitches.push(lineSwitch);
48 if (lineSwitch.outOfRange)
49 continue;
50 const rulesToSwitch = new Map();
51 for (const rawRuleSwitch of rawLineSwitch.rules) {
52 const ruleSwitch = {
53 location: rawRuleSwitch.location,
54 fixLocation: rawRuleSwitch.fixLocation || rawRuleSwitch.location,
55 state: 0 /* NoMatch */,
56 };
57 lineSwitch.rules.push(ruleSwitch);
58 if (typeof rawRuleSwitch.predicate === 'string') {
59 if (ruleNames.includes(rawRuleSwitch.predicate)) {
60 if (rulesToSwitch.has(rawRuleSwitch.predicate)) {
61 ruleSwitch.state = 2 /* Redundant */;
62 }
63 else {
64 rulesToSwitch.set(rawRuleSwitch.predicate, ruleSwitch);
65 ruleSwitch.state = 1 /* NoChange */;
66 }
67 }
68 }
69 else {
70 const matchingNames = ruleNames.filter(makeFilterPredicate(rawRuleSwitch.predicate));
71 if (matchingNames.length !== 0) {
72 ruleSwitch.state = 2 /* Redundant */;
73 for (const rule of matchingNames) {
74 if (!rulesToSwitch.has(rule)) {
75 rulesToSwitch.set(rule, ruleSwitch);
76 ruleSwitch.state = 1 /* NoChange */;
77 }
78 }
79 }
80 }
81 }
82 for (const [rule, ruleSwitch] of rulesToSwitch) {
83 const ranges = result.get(rule);
84 if (ranges === undefined) {
85 if (rawLineSwitch.enable)
86 continue; // rule is already enabled
87 result.set(rule, [{ pos: rawLineSwitch.pos, end: (_a = rawLineSwitch.end) !== null && _a !== void 0 ? _a : Infinity, switch: ruleSwitch }]);
88 }
89 else {
90 const last = ranges[ranges.length - 1];
91 if (last.end === Infinity) {
92 if (!rawLineSwitch.enable)
93 continue; // rule is already disabled
94 last.end = rawLineSwitch.pos;
95 if (rawLineSwitch.end !== undefined)
96 ranges.push({ pos: rawLineSwitch.end, end: Infinity, switch: ruleSwitch });
97 }
98 else if (rawLineSwitch.enable || rawLineSwitch.pos < last.end) {
99 // rule is already enabled
100 // or disabled range is nested inside the previous range
101 continue;
102 }
103 else {
104 ranges.push({
105 pos: rawLineSwitch.pos,
106 end: (_b = rawLineSwitch.end) !== null && _b !== void 0 ? _b : Infinity,
107 switch: ruleSwitch,
108 });
109 }
110 }
111 ruleSwitch.state = 3 /* Unused */;
112 }
113 }
114 return { switches: lineSwitches, disables: result };
115 }
116};
117LineSwitchFilterFactory = tslib_1.__decorate([
118 inversify_1.injectable(),
119 tslib_1.__metadata("design:paramtypes", [ymir_1.LineSwitchParser])
120], LineSwitchFilterFactory);
121exports.LineSwitchFilterFactory = LineSwitchFilterFactory;
122const stateText = {
123 [1 /* NoChange */](singular, mode) { return `${singular ? 'is' : 'are'} already ${mode}d`; },
124 [0 /* NoMatch */](singular) { return `do${singular ? 'es' : ''}n't match any rules enabled for this file`; },
125 [3 /* Unused */](singular) { return `${singular ? 'has' : 'have'} no failures to disable`; },
126 [2 /* Redundant */](singular, mode) { return singular ? `was already specified in this ${mode} switch` : 'are redundant'; },
127};
128class Filter {
129 constructor(disables, switches, sourceFile) {
130 this.disables = disables;
131 this.switches = switches;
132 this.sourceFile = sourceFile;
133 }
134 filter(finding) {
135 const ruleDisables = this.disables.get(finding.ruleName);
136 if (ruleDisables !== undefined) {
137 const { start: { position: pos }, end: { position: end } } = finding;
138 for (const disabledRange of ruleDisables) {
139 if (end > disabledRange.pos && pos < disabledRange.end) {
140 disabledRange.switch.state = 4 /* Used */;
141 return false;
142 }
143 }
144 }
145 return true;
146 }
147 reportUseless(severity) {
148 const result = [];
149 for (const current of this.switches) {
150 const mode = current.enable ? 'enable' : 'disable';
151 if (current.rules.length === 0) {
152 result.push(this.createFinding(current.outOfRange
153 ? `${titlecase(mode)} switch has no effect. The specified range doesn't exits.`
154 : `${titlecase(mode)} switch doesn't specify any rule names.`, severity, current.location));
155 continue;
156 }
157 const counts = new Array(4 /* Used */ + 1).fill(0);
158 for (const rule of current.rules)
159 ++counts[rule.state];
160 if (counts[4 /* Used */] === 0 && (!current.enable || counts[3 /* Unused */] === 0)) {
161 const errorStates = [];
162 for (let state = 0 /* NoMatch */; state !== 4 /* Used */; ++state)
163 if (counts[state] !== 0)
164 errorStates.push(stateText[state](false, mode));
165 result.push(this.createFinding(`${titlecase(mode)} switch has no effect. All specified rules ${join(errorStates)}.`, severity, current.location));
166 continue;
167 }
168 for (const ruleSwitch of current.rules)
169 if (ruleSwitch.location !== undefined &&
170 ruleSwitch.state !== 4 /* Used */ &&
171 (!current.enable || ruleSwitch.state !== 3 /* Unused */))
172 result.push(this.createFinding(`This rule ${stateText[ruleSwitch.state](true, mode)}.`, severity, ruleSwitch.location, ruleSwitch.fixLocation));
173 }
174 return result;
175 }
176 createPosition(pos) {
177 return {
178 position: pos,
179 ...ts.getLineAndCharacterOfPosition(this.sourceFile, pos),
180 };
181 }
182 createFinding(message, severity, location, fixLocation = location) {
183 return {
184 ruleName: 'useless-line-switch',
185 severity,
186 message,
187 start: this.createPosition(location.pos),
188 end: this.createPosition(location.end),
189 fix: { replacements: [ymir_1.Replacement.delete(fixLocation.pos, fixLocation.end)] },
190 };
191 }
192}
193function titlecase(str) {
194 return str.charAt(0).toUpperCase() + str.substr(1);
195}
196function join(parts) {
197 if (parts.length === 1)
198 return parts[0];
199 return parts.slice(0, -1).join(', ') + ' or ' + parts[parts.length - 1];
200}
201function makeFilterPredicate(predicate) {
202 return typeof predicate === 'function' ? predicate : (ruleName) => predicate.test(ruleName);
203}
204let DefaultLineSwitchParser = class DefaultLineSwitchParser {
205 parse(context) {
206 const { sourceFile } = context;
207 const result = [];
208 const commentRegex = /(\/[/*] *wotan-(enable|disable)((?:-next)?-line)?)( +(?:(?:[\w-]+\/)*[\w-]+ *, *)*(?:[\w-]+\/)*[\w-]+)? *(?:$|\*\/)/mg;
209 for (let match = commentRegex.exec(sourceFile.text); match !== null; match = commentRegex.exec(sourceFile.text)) {
210 const comment = context.getCommentAtPosition(match.index);
211 if (comment === undefined || comment.pos !== match.index || comment.end !== match.index + match[0].length)
212 continue;
213 const rules = match[4] === undefined ? [{ predicate: /^/ }] : parseRules(match[4], match.index + match[1].length);
214 const enable = match[2] === 'enable';
215 switch (match[3]) {
216 case '-line': {
217 const lineStarts = sourceFile.getLineStarts();
218 const { line } = ts.getLineAndCharacterOfPosition(sourceFile, comment.pos);
219 result.push({
220 rules,
221 enable,
222 pos: lineStarts[line],
223 // no need to switch back if there is no next line
224 end: lineStarts.length === line + 1 ? undefined : lineStarts[line + 1],
225 location: { pos: comment.pos, end: comment.end },
226 });
227 break;
228 }
229 case '-next-line': {
230 const lineStarts = sourceFile.getLineStarts();
231 const line = ts.getLineAndCharacterOfPosition(sourceFile, comment.pos).line + 1;
232 if (lineStarts.length === line) {
233 // there is no next line, return an out-of-range switch that can be reported
234 result.push({
235 rules,
236 enable,
237 pos: sourceFile.end + 1,
238 end: undefined,
239 location: { pos: comment.pos, end: comment.end },
240 });
241 }
242 else {
243 result.push({
244 rules,
245 enable,
246 pos: lineStarts[line],
247 // no need to switch back if there is no next line
248 end: lineStarts.length === line + 1 ? undefined : lineStarts[line + 1],
249 location: { pos: comment.pos, end: comment.end },
250 });
251 }
252 break;
253 }
254 default:
255 result.push({ rules, enable, pos: comment.pos, end: undefined, location: { pos: comment.pos, end: comment.end } });
256 }
257 }
258 return result;
259 }
260};
261DefaultLineSwitchParser = tslib_1.__decorate([
262 inversify_1.injectable()
263], DefaultLineSwitchParser);
264exports.DefaultLineSwitchParser = DefaultLineSwitchParser;
265function parseRules(raw, offset) {
266 const result = [];
267 const re = /(?: *, *|$)/g;
268 let pos = raw.search(/[^ ]/);
269 let fixPos = pos;
270 for (let match = re.exec(raw);; match = re.exec(raw)) {
271 result.push({
272 predicate: raw.slice(pos, match.index),
273 location: { pos: pos + offset, end: match.index + offset },
274 // fix of first rule needs to remove the comma after it, all other rule fixes need to remove the comma before it
275 fixLocation: { pos: fixPos + offset, end: (result.length === 0 ? re.lastIndex : match.index) + offset },
276 });
277 if (match[0].length === 0)
278 break;
279 pos = re.lastIndex;
280 fixPos = match.index; // fix always removes the preceeding comma
281 }
282 return result;
283}
284//# sourceMappingURL=line-switches.js.map
\No newline at end of file