UNPKG

@fimbul/wotan

Version:

Pluggable TypeScript and JavaScript linter

284 lines 13.3 kB
"use strict"; 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