1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | exports.DefaultLineSwitchParser = exports.LineSwitchFilterFactory = exports.LINE_SWITCH_REGEX = void 0;
|
4 | const tslib_1 = require("tslib");
|
5 | const inversify_1 = require("inversify");
|
6 | const ts = require("typescript");
|
7 | const tsutils_1 = require("tsutils");
|
8 | const ymir_1 = require("@fimbul/ymir");
|
9 | exports.LINE_SWITCH_REGEX = /^ *wotan-(enable|disable)((?:-next)?-line)?( +(?:(?:[\w-]+\/)*[\w-]+ *, *)*(?:[\w-]+\/)*[\w-]+)? *$/;
|
10 | let 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 |
|
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 ,
|
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 ;
|
62 | }
|
63 | else {
|
64 | rulesToSwitch.set(rawRuleSwitch.predicate, ruleSwitch);
|
65 | ruleSwitch.state = 1 ;
|
66 | }
|
67 | }
|
68 | }
|
69 | else {
|
70 | const matchingNames = ruleNames.filter(makeFilterPredicate(rawRuleSwitch.predicate));
|
71 | if (matchingNames.length !== 0) {
|
72 | ruleSwitch.state = 2 ;
|
73 | for (const rule of matchingNames) {
|
74 | if (!rulesToSwitch.has(rule)) {
|
75 | rulesToSwitch.set(rule, ruleSwitch);
|
76 | ruleSwitch.state = 1 ;
|
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;
|
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;
|
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 |
|
100 |
|
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 ;
|
112 | }
|
113 | }
|
114 | return { switches: lineSwitches, disables: result };
|
115 | }
|
116 | };
|
117 | LineSwitchFilterFactory = tslib_1.__decorate([
|
118 | inversify_1.injectable(),
|
119 | tslib_1.__metadata("design:paramtypes", [ymir_1.LineSwitchParser])
|
120 | ], LineSwitchFilterFactory);
|
121 | exports.LineSwitchFilterFactory = LineSwitchFilterFactory;
|
122 | const stateText = {
|
123 | [1 ](singular, mode) { return `${singular ? 'is' : 'are'} already ${mode}d`; },
|
124 | [0 ](singular) { return `do${singular ? 'es' : ''}n't match any rules enabled for this file`; },
|
125 | [3 ](singular) { return `${singular ? 'has' : 'have'} no failures to disable`; },
|
126 | [2 ](singular, mode) { return singular ? `was already specified in this ${mode} switch` : 'are redundant'; },
|
127 | };
|
128 | class 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 ;
|
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 + 1).fill(0);
|
158 | for (const rule of current.rules)
|
159 | ++counts[rule.state];
|
160 | if (counts[4 ] === 0 && (!current.enable || counts[3 ] === 0)) {
|
161 | const errorStates = [];
|
162 | for (let state = 0 ; state !== 4 ; ++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 &&
|
171 | (!current.enable || ruleSwitch.state !== 3 ))
|
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 | }
|
193 | function titlecase(str) {
|
194 | return str.charAt(0).toUpperCase() + str.substr(1);
|
195 | }
|
196 | function join(parts) {
|
197 | if (parts.length === 1)
|
198 | return parts[0];
|
199 | return parts.slice(0, -1).join(', ') + ' or ' + parts[parts.length - 1];
|
200 | }
|
201 | function makeFilterPredicate(predicate) {
|
202 | return typeof predicate === 'function' ? predicate : (ruleName) => predicate.test(ruleName);
|
203 | }
|
204 | let 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 |
|
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 |
|
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 |
|
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 | };
|
261 | DefaultLineSwitchParser = tslib_1.__decorate([
|
262 | inversify_1.injectable()
|
263 | ], DefaultLineSwitchParser);
|
264 | exports.DefaultLineSwitchParser = DefaultLineSwitchParser;
|
265 | function 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 |
|
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;
|
281 | }
|
282 | return result;
|
283 | }
|
284 |
|
\ | No newline at end of file |