@fimbul/wotan
Version:
Pluggable TypeScript and JavaScript linter
284 lines • 13.3 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.DefaultLineSwitchParser = exports.LineSwitchFilterFactory = exports.LINE_SWITCH_REGEX = void 0;
const tslib_1 = require("tslib");
const inversify_1 = require("inversify");
const ts = require("typescript");
const tsutils_1 = require("tsutils");
const ymir_1 = require("@fimbul/ymir");
exports.LINE_SWITCH_REGEX = /^ *wotan-(enable|disable)((?:-next)?-line)?( +(?:(?:[\w-]+\/)*[\w-]+ *, *)*(?:[\w-]+\/)*[\w-]+)? *$/;
let LineSwitchFilterFactory = class LineSwitchFilterFactory {
constructor(parser) {
this.parser = parser;
}
create(context) {
const { disables, switches } = this.parseLineSwitches(context);
return new Filter(disables, switches, context.sourceFile);
}
getDisabledRanges(context) {
// remove internal `switch` property from ranges
return new Map(Array.from(this.parseLineSwitches(context).disables, (entry) => [
entry[0],
entry[1].map((range) => ({ pos: range.pos, end: range.end })),
]));
}
parseLineSwitches(context) {
var _a, _b;
const { sourceFile, ruleNames } = context;
let wrappedAst;
const raw = this.parser.parse({
sourceFile,
getCommentAtPosition(pos) {
const wrap = tsutils_1.getWrappedNodeAtPosition(wrappedAst !== null && wrappedAst !== void 0 ? wrappedAst : (wrappedAst = context.getWrappedAst()), pos);
if (wrap === undefined)
return;
return tsutils_1.getCommentAtPosition(sourceFile, pos, wrap.node);
},
});
const lineSwitches = [];
const result = new Map();
for (const rawLineSwitch of raw) {
const lineSwitch = {
location: rawLineSwitch.location,
enable: rawLineSwitch.enable,
rules: [],
outOfRange: rawLineSwitch.end <= 0 || rawLineSwitch.pos > sourceFile.end,
};
lineSwitches.push(lineSwitch);
if (lineSwitch.outOfRange)
continue;
const rulesToSwitch = new Map();
for (const rawRuleSwitch of rawLineSwitch.rules) {
const ruleSwitch = {
location: rawRuleSwitch.location,
fixLocation: rawRuleSwitch.fixLocation || rawRuleSwitch.location,
state: 0 /* NoMatch */,
};
lineSwitch.rules.push(ruleSwitch);
if (typeof rawRuleSwitch.predicate === 'string') {
if (ruleNames.includes(rawRuleSwitch.predicate)) {
if (rulesToSwitch.has(rawRuleSwitch.predicate)) {
ruleSwitch.state = 2 /* Redundant */;
}
else {
rulesToSwitch.set(rawRuleSwitch.predicate, ruleSwitch);
ruleSwitch.state = 1 /* NoChange */;
}
}
}
else {
const matchingNames = ruleNames.filter(makeFilterPredicate(rawRuleSwitch.predicate));
if (matchingNames.length !== 0) {
ruleSwitch.state = 2 /* Redundant */;
for (const rule of matchingNames) {
if (!rulesToSwitch.has(rule)) {
rulesToSwitch.set(rule, ruleSwitch);
ruleSwitch.state = 1 /* NoChange */;
}
}
}
}
}
for (const [rule, ruleSwitch] of rulesToSwitch) {
const ranges = result.get(rule);
if (ranges === undefined) {
if (rawLineSwitch.enable)
continue; // rule is already enabled
result.set(rule, [{ pos: rawLineSwitch.pos, end: (_a = rawLineSwitch.end) !== null && _a !== void 0 ? _a : Infinity, switch: ruleSwitch }]);
}
else {
const last = ranges[ranges.length - 1];
if (last.end === Infinity) {
if (!rawLineSwitch.enable)
continue; // rule is already disabled
last.end = rawLineSwitch.pos;
if (rawLineSwitch.end !== undefined)
ranges.push({ pos: rawLineSwitch.end, end: Infinity, switch: ruleSwitch });
}
else if (rawLineSwitch.enable || rawLineSwitch.pos < last.end) {
// rule is already enabled
// or disabled range is nested inside the previous range
continue;
}
else {
ranges.push({
pos: rawLineSwitch.pos,
end: (_b = rawLineSwitch.end) !== null && _b !== void 0 ? _b : Infinity,
switch: ruleSwitch,
});
}
}
ruleSwitch.state = 3 /* Unused */;
}
}
return { switches: lineSwitches, disables: result };
}
};
LineSwitchFilterFactory = tslib_1.__decorate([
inversify_1.injectable(),
tslib_1.__metadata("design:paramtypes", [ymir_1.LineSwitchParser])
], LineSwitchFilterFactory);
exports.LineSwitchFilterFactory = LineSwitchFilterFactory;
const stateText = {
[1 /* NoChange */](singular, mode) { return `${singular ? 'is' : 'are'} already ${mode}d`; },
[0 /* NoMatch */](singular) { return `do${singular ? 'es' : ''}n't match any rules enabled for this file`; },
[3 /* Unused */](singular) { return `${singular ? 'has' : 'have'} no failures to disable`; },
[2 /* Redundant */](singular, mode) { return singular ? `was already specified in this ${mode} switch` : 'are redundant'; },
};
class Filter {
constructor(disables, switches, sourceFile) {
this.disables = disables;
this.switches = switches;
this.sourceFile = sourceFile;
}
filter(finding) {
const ruleDisables = this.disables.get(finding.ruleName);
if (ruleDisables !== undefined) {
const { start: { position: pos }, end: { position: end } } = finding;
for (const disabledRange of ruleDisables) {
if (end > disabledRange.pos && pos < disabledRange.end) {
disabledRange.switch.state = 4 /* Used */;
return false;
}
}
}
return true;
}
reportUseless(severity) {
const result = [];
for (const current of this.switches) {
const mode = current.enable ? 'enable' : 'disable';
if (current.rules.length === 0) {
result.push(this.createFinding(current.outOfRange
? `${titlecase(mode)} switch has no effect. The specified range doesn't exits.`
: `${titlecase(mode)} switch doesn't specify any rule names.`, severity, current.location));
continue;
}
const counts = new Array(4 /* Used */ + 1).fill(0);
for (const rule of current.rules)
++counts[rule.state];
if (counts[4 /* Used */] === 0 && (!current.enable || counts[3 /* Unused */] === 0)) {
const errorStates = [];
for (let state = 0 /* NoMatch */; state !== 4 /* Used */; ++state)
if (counts[state] !== 0)
errorStates.push(stateText[state](false, mode));
result.push(this.createFinding(`${titlecase(mode)} switch has no effect. All specified rules ${join(errorStates)}.`, severity, current.location));
continue;
}
for (const ruleSwitch of current.rules)
if (ruleSwitch.location !== undefined &&
ruleSwitch.state !== 4 /* Used */ &&
(!current.enable || ruleSwitch.state !== 3 /* Unused */))
result.push(this.createFinding(`This rule ${stateText[ruleSwitch.state](true, mode)}.`, severity, ruleSwitch.location, ruleSwitch.fixLocation));
}
return result;
}
createPosition(pos) {
return {
position: pos,
...ts.getLineAndCharacterOfPosition(this.sourceFile, pos),
};
}
createFinding(message, severity, location, fixLocation = location) {
return {
ruleName: 'useless-line-switch',
severity,
message,
start: this.createPosition(location.pos),
end: this.createPosition(location.end),
fix: { replacements: [ymir_1.Replacement.delete(fixLocation.pos, fixLocation.end)] },
};
}
}
function titlecase(str) {
return str.charAt(0).toUpperCase() + str.substr(1);
}
function join(parts) {
if (parts.length === 1)
return parts[0];
return parts.slice(0, -1).join(', ') + ' or ' + parts[parts.length - 1];
}
function makeFilterPredicate(predicate) {
return typeof predicate === 'function' ? predicate : (ruleName) => predicate.test(ruleName);
}
let DefaultLineSwitchParser = class DefaultLineSwitchParser {
parse(context) {
const { sourceFile } = context;
const result = [];
const commentRegex = /(\/[/*] *wotan-(enable|disable)((?:-next)?-line)?)( +(?:(?:[\w-]+\/)*[\w-]+ *, *)*(?:[\w-]+\/)*[\w-]+)? *(?:$|\*\/)/mg;
for (let match = commentRegex.exec(sourceFile.text); match !== null; match = commentRegex.exec(sourceFile.text)) {
const comment = context.getCommentAtPosition(match.index);
if (comment === undefined || comment.pos !== match.index || comment.end !== match.index + match[0].length)
continue;
const rules = match[4] === undefined ? [{ predicate: /^/ }] : parseRules(match[4], match.index + match[1].length);
const enable = match[2] === 'enable';
switch (match[3]) {
case '-line': {
const lineStarts = sourceFile.getLineStarts();
const { line } = ts.getLineAndCharacterOfPosition(sourceFile, comment.pos);
result.push({
rules,
enable,
pos: lineStarts[line],
// no need to switch back if there is no next line
end: lineStarts.length === line + 1 ? undefined : lineStarts[line + 1],
location: { pos: comment.pos, end: comment.end },
});
break;
}
case '-next-line': {
const lineStarts = sourceFile.getLineStarts();
const line = ts.getLineAndCharacterOfPosition(sourceFile, comment.pos).line + 1;
if (lineStarts.length === line) {
// there is no next line, return an out-of-range switch that can be reported
result.push({
rules,
enable,
pos: sourceFile.end + 1,
end: undefined,
location: { pos: comment.pos, end: comment.end },
});
}
else {
result.push({
rules,
enable,
pos: lineStarts[line],
// no need to switch back if there is no next line
end: lineStarts.length === line + 1 ? undefined : lineStarts[line + 1],
location: { pos: comment.pos, end: comment.end },
});
}
break;
}
default:
result.push({ rules, enable, pos: comment.pos, end: undefined, location: { pos: comment.pos, end: comment.end } });
}
}
return result;
}
};
DefaultLineSwitchParser = tslib_1.__decorate([
inversify_1.injectable()
], DefaultLineSwitchParser);
exports.DefaultLineSwitchParser = DefaultLineSwitchParser;
function parseRules(raw, offset) {
const result = [];
const re = /(?: *, *|$)/g;
let pos = raw.search(/[^ ]/);
let fixPos = pos;
for (let match = re.exec(raw);; match = re.exec(raw)) {
result.push({
predicate: raw.slice(pos, match.index),
location: { pos: pos + offset, end: match.index + offset },
// fix of first rule needs to remove the comma after it, all other rule fixes need to remove the comma before it
fixLocation: { pos: fixPos + offset, end: (result.length === 0 ? re.lastIndex : match.index) + offset },
});
if (match[0].length === 0)
break;
pos = re.lastIndex;
fixPos = match.index; // fix always removes the preceeding comma
}
return result;
}
//# sourceMappingURL=line-switches.js.map
;