@fimbul/wotan
Advanced tools
Comparing version
@@ -130,3 +130,9 @@ "use strict"; | ||
const linter = container.get(linter_1.Linter); | ||
return linter.lintFile(file, effectiveConfig, program); | ||
return linter.lintFile(file, effectiveConfig, program, { | ||
reportUselessDirectives: globalConfig.reportUselessDirectives | ||
? globalConfig.reportUselessDirectives === true | ||
? 'error' | ||
: globalConfig.reportUselessDirectives | ||
: undefined, | ||
}); | ||
} | ||
@@ -133,0 +139,0 @@ loadPluginModule(moduleName, basedir, options) { |
{ | ||
"name": "@fimbul/wotan", | ||
"version": "0.18.0-dev.20190103", | ||
"version": "0.18.0-dev.20190106", | ||
"description": "Pluggable TypeScript and JavaScript linter", | ||
@@ -47,4 +47,4 @@ "bin": "bin/main.js", | ||
"dependencies": { | ||
"@fimbul/mimir": "0.17.0", | ||
"@fimbul/ymir": "0.17.0", | ||
"@fimbul/mimir": "0.18.0-dev.20190106", | ||
"@fimbul/ymir": "0.18.0-dev.20190106", | ||
"bind-decorator": "^1.0.11", | ||
@@ -51,0 +51,0 @@ "chalk": "^2.3.0", |
@@ -137,2 +137,3 @@ # Wotan | ||
Sometimes you need to enable or disable a specific rule or all rules for a section of a file. This can be done using comments. It doesn't matter if you use `//` or `/* */`. Multiple rule names are separated by comma. | ||
It's not possible to enable a rule with a comment if that rule is not already enabled in the configuration for that file. That means comments can only enable rules that were previously disabled by a comment. | ||
@@ -148,2 +149,4 @@ * `// wotan-disable` disables all rules from the start of the comment until the end of the file (or until it is enabled again) | ||
To detect unused or redundant comments you can use the `--report-useless-directives` CLI option. | ||
## CLI Options | ||
@@ -158,2 +161,3 @@ | ||
* `-r --references [true|false]` enables project references. Starting from the project specified with `-p --project` or the `tsconfig.json` in the current directory it will recursively follow all `"references"` and lint those projects. | ||
* `--report-useless-directives [true|false|error|warning|suggestion]` reports `// wotan-disable` and `// wotan-enable` comments that are redundant (i.e. rules are already disabled) or unused (there are no findings for the specified rules). Useless directives are reported as lint findings with the specified severity (`true` is converted to `error`). Those findings cannot be disabled by a disable comment. The findings are fixable which allow autofixing when used with the `--fix` option. | ||
* `[...FILES]` specifies the files to lint. You can specify paths and glob patterns here. | ||
@@ -160,0 +164,0 @@ |
@@ -5,16 +5,17 @@ import { GlobalOptions } from '@fimbul/ymir'; | ||
export interface ParsedGlobalOptions extends LintOptions { | ||
modules: string[]; | ||
modules: ReadonlyArray<string>; | ||
formatter: string | undefined; | ||
} | ||
export declare const GLOBAL_OPTIONS_SPEC: { | ||
modules: OptionParser.ParseFunction<string[]>; | ||
modules: OptionParser.ParseFunction<ReadonlyArray<string>>; | ||
config: OptionParser.ParseFunction<string | undefined>; | ||
files: OptionParser.ParseFunction<string[]>; | ||
exclude: OptionParser.ParseFunction<string[]>; | ||
project: OptionParser.ParseFunction<string[]>; | ||
files: OptionParser.ParseFunction<ReadonlyArray<string>>; | ||
exclude: OptionParser.ParseFunction<ReadonlyArray<string>>; | ||
project: OptionParser.ParseFunction<ReadonlyArray<string>>; | ||
references: OptionParser.ParseFunction<boolean>; | ||
formatter: OptionParser.ParseFunction<string | undefined>; | ||
fix: OptionParser.ParseFunction<number | boolean>; | ||
extensions: OptionParser.ParseFunction<string[] | undefined>; | ||
extensions: OptionParser.ParseFunction<ReadonlyArray<string> | undefined>; | ||
reportUselessDirectives: OptionParser.ParseFunction<boolean | "warning" | "error" | "suggestion">; | ||
}; | ||
export declare function parseGlobalOptions(options: GlobalOptions | undefined): ParsedGlobalOptions; |
@@ -48,2 +48,21 @@ "use strict"; | ||
extensions: optparse_1.OptionParser.Transform.map(optparse_1.OptionParser.Factory.parsePrimitiveOrArray('string'), sanitizeExtensionArgument), | ||
reportUselessDirectives: optparse_1.OptionParser.Transform.transform(optparse_1.OptionParser.Factory.parsePrimitive('string', 'boolean'), (value) => { | ||
switch (value) { | ||
case true: | ||
case false: | ||
case 'error': | ||
case 'warning': | ||
case 'suggestion': | ||
return value; | ||
case 'warn': | ||
return 'warning'; | ||
case 'hint': | ||
return 'suggestion'; | ||
case undefined: | ||
case 'off': | ||
return false; | ||
default: | ||
return 'error'; | ||
} | ||
}), | ||
}; | ||
@@ -102,2 +121,5 @@ function parseGlobalOptions(options) { | ||
break; | ||
case '--report-useless-directives': | ||
({ index: i, argument: result.reportUselessDirectives } = parseOptionalSeverityOrBoolean(args, i)); | ||
break; | ||
case '--': | ||
@@ -133,5 +155,6 @@ result.files = files; | ||
function parseTestCommand(args) { | ||
const modules = []; | ||
const result = { | ||
modules, | ||
command: "test", | ||
modules: [], | ||
bail: false, | ||
@@ -157,3 +180,3 @@ files: [], | ||
case '--module': | ||
result.modules.push(...expectStringArgument(args, ++i, arg).split(/,/g).filter(isTruthy)); | ||
modules.push(...expectStringArgument(args, ++i, arg).split(/,/g).filter(isTruthy)); | ||
break; | ||
@@ -240,2 +263,22 @@ case '--': | ||
} | ||
function parseOptionalSeverityOrBoolean(args, index) { | ||
if (index + 1 !== args.length) { | ||
switch (args[index + 1]) { | ||
case 'true': | ||
return { index: index + 1, argument: true }; | ||
case 'false': | ||
case 'off': | ||
return { index: index + 1, argument: false }; | ||
case 'error': | ||
return { index: index + 1, argument: 'error' }; | ||
case 'warn': | ||
case 'warning': | ||
return { index: index + 1, argument: 'warning' }; | ||
case 'hint': | ||
case 'suggestion': | ||
return { index: index + 1, argument: 'suggestion' }; | ||
} | ||
} | ||
return { index, argument: true }; | ||
} | ||
function parseOptionalBoolean(args, index) { | ||
@@ -242,0 +285,0 @@ if (index + 1 !== args.length) { |
@@ -20,3 +20,3 @@ "use strict"; | ||
var { command: _command } = _a, config = tslib_1.__rest(_a, ["command"]); | ||
const newContent = utils_1.format(Object.assign({}, this.options, config, { fix: config.fix || undefined, references: config.references || undefined }), "yaml"); | ||
const newContent = utils_1.format(Object.assign({}, this.options, config, { fix: config.fix || undefined, reportUselessDirectives: config.reportUselessDirectives || undefined, references: config.references || undefined }), "yaml"); | ||
const filePath = path.join(this.directories.getCurrentDirectory(), '.fimbullinter.yaml'); | ||
@@ -23,0 +23,0 @@ if (newContent.trim() === '{}') { |
import * as ts from 'typescript'; | ||
import { Finding, EffectiveConfiguration, LintAndFixFileResult, MessageHandler, AbstractProcessor, DeprecationHandler, FindingFilterFactory } from '@fimbul/ymir'; | ||
import { Finding, EffectiveConfiguration, LintAndFixFileResult, Severity, MessageHandler, AbstractProcessor, DeprecationHandler, FindingFilterFactory } from '@fimbul/ymir'; | ||
import { RuleLoader } from './services/rule-loader'; | ||
@@ -8,2 +8,5 @@ export interface UpdateFileResult { | ||
} | ||
export interface LinterOptions { | ||
reportUselessDirectives?: Severity; | ||
} | ||
export declare type UpdateFileCallback = (content: string, range: ts.TextChangeRange) => UpdateFileResult | undefined; | ||
@@ -16,6 +19,6 @@ export declare class Linter { | ||
constructor(ruleLoader: RuleLoader, logger: MessageHandler, deprecationHandler: DeprecationHandler, filterFactory: FindingFilterFactory); | ||
lintFile(file: ts.SourceFile, config: EffectiveConfiguration, program?: ts.Program): ReadonlyArray<Finding>; | ||
lintAndFix(file: ts.SourceFile, content: string, config: EffectiveConfiguration, updateFile: UpdateFileCallback, iterations?: number, program?: ts.Program, processor?: AbstractProcessor): LintAndFixFileResult; | ||
lintFile(file: ts.SourceFile, config: EffectiveConfiguration, program?: ts.Program, options?: LinterOptions): ReadonlyArray<Finding>; | ||
lintAndFix(file: ts.SourceFile, content: string, config: EffectiveConfiguration, updateFile: UpdateFileCallback, iterations?: number, program?: ts.Program, processor?: AbstractProcessor, options?: LinterOptions): LintAndFixFileResult; | ||
private prepareRules; | ||
private applyRules; | ||
} |
@@ -20,8 +20,8 @@ "use strict"; | ||
} | ||
lintFile(file, config, program) { | ||
return this.getFindings(file, config, program, undefined); | ||
lintFile(file, config, program, options = {}) { | ||
return this.getFindings(file, config, program, undefined, options); | ||
} | ||
lintAndFix(file, content, config, updateFile, iterations = 10, program, processor) { | ||
lintAndFix(file, content, config, updateFile, iterations = 10, program, processor, options = {}) { | ||
let totalFixes = 0; | ||
let findings = this.getFindings(file, config, program, processor); | ||
let findings = this.getFindings(file, config, program, processor, options); | ||
for (let i = 0; i < iterations; ++i) { | ||
@@ -59,3 +59,3 @@ if (findings.length === 0) | ||
totalFixes += fixed.fixed; | ||
findings = this.getFindings(file, config, program, processor); | ||
findings = this.getFindings(file, config, program, processor, options); | ||
} | ||
@@ -68,3 +68,3 @@ return { | ||
} | ||
getFindings(sourceFile, config, program, processor) { | ||
getFindings(sourceFile, config, program, processor, options) { | ||
let suppressMissingTypeInfoWarning = false; | ||
@@ -81,7 +81,16 @@ log('Linting file %s', sourceFile.fileName); | ||
const rules = this.prepareRules(config, sourceFile, program, suppressMissingTypeInfoWarning); | ||
let findings; | ||
if (rules.length === 0) { | ||
log('No active rules'); | ||
return processor === undefined ? [] : processor.postprocess([]); | ||
if (options.reportUselessDirectives !== undefined) { | ||
findings = this.filterFactory | ||
.create({ sourceFile, getWrappedAst() { return tsutils_1.convertAst(sourceFile).wrapped; }, ruleNames: [] }) | ||
.reportUseless(options.reportUselessDirectives); | ||
log('Found %d useless directives', findings.length); | ||
} | ||
else { | ||
findings = []; | ||
} | ||
} | ||
const findings = this.applyRules(sourceFile, program, rules, config.settings); | ||
findings = this.applyRules(sourceFile, program, rules, config.settings, options); | ||
return processor === undefined ? findings : processor.postprocess(findings); | ||
@@ -116,3 +125,3 @@ } | ||
} | ||
applyRules(sourceFile, program, rules, settings) { | ||
applyRules(sourceFile, program, rules, settings, options) { | ||
const result = []; | ||
@@ -124,2 +133,6 @@ let findingFilter; | ||
let convertedAst; | ||
const getFindingFilter = () => { | ||
return findingFilter || | ||
(findingFilter = this.filterFactory.create({ sourceFile, getWrappedAst, ruleNames: rules.map((r) => r.ruleName) })); | ||
}; | ||
const addFinding = (pos, end, message, fix) => { | ||
@@ -140,5 +153,3 @@ const finding = { | ||
}; | ||
if (findingFilter === undefined) | ||
findingFilter = this.filterFactory.create({ sourceFile, getWrappedAst, ruleNames: rules.map((r) => r.ruleName) }); | ||
if (findingFilter.filter(finding)) | ||
if (getFindingFilter().filter(finding)) | ||
result.push(finding); | ||
@@ -160,2 +171,7 @@ }; | ||
log('Found %d findings', result.length); | ||
if (options.reportUselessDirectives !== undefined) { | ||
const useless = getFindingFilter().reportUseless(options.reportUselessDirectives); | ||
log('Found %d useless directives', useless.length); | ||
result.push(...useless); | ||
} | ||
return result; | ||
@@ -162,0 +178,0 @@ function getFlatAst() { |
@@ -16,5 +16,6 @@ export declare namespace OptionParser { | ||
function noDefault<T>(parseFn: ParseFunction<T>): ParseFunction<T | undefined>; | ||
function map<T extends U[] | undefined, U, V>(parseFn: ParseFunction<T>, cb: (item: U) => V): ParseFunction<{ | ||
function map<T extends ReadonlyArray<U> | undefined, U, V>(parseFn: ParseFunction<T>, cb: (item: U) => V): ParseFunction<{ | ||
[K in keyof T]: V; | ||
}>; | ||
function transform<T, U>(parseFn: ParseFunction<T>, cb: (value: T) => U): ParseFunction<U>; | ||
} | ||
@@ -25,4 +26,4 @@ namespace Factory { | ||
function parsePrimitive<T extends PrimitiveName[]>(...types: T): ParseFunction<PrimitiveMap<T[number]> | undefined>; | ||
function parsePrimitiveOrArray<T extends PrimitiveName>(type: T): ParseFunction<Array<PrimitiveMap<T>> | undefined>; | ||
function parsePrimitiveOrArray<T extends PrimitiveName>(type: T): ParseFunction<ReadonlyArray<PrimitiveMap<T>> | undefined>; | ||
} | ||
} |
@@ -51,2 +51,8 @@ "use strict"; | ||
Transform.map = map; | ||
function transform(parseFn, cb) { | ||
return (value, report) => { | ||
return cb(parseFn(value, report)); | ||
}; | ||
} | ||
Transform.transform = transform; | ||
})(Transform = OptionParser.Transform || (OptionParser.Transform = {})); | ||
@@ -53,0 +59,0 @@ let Factory; |
import { Linter } from './linter'; | ||
import { LintResult, DirectoryService, MessageHandler, FileFilterFactory } from '@fimbul/ymir'; | ||
import { LintResult, DirectoryService, MessageHandler, FileFilterFactory, Severity } from '@fimbul/ymir'; | ||
import * as ts from 'typescript'; | ||
@@ -15,2 +15,3 @@ import { ProcessorLoader } from './services/processor-loader'; | ||
extensions: ReadonlyArray<string> | undefined; | ||
reportUselessDirectives: Severity | boolean | undefined; | ||
} | ||
@@ -17,0 +18,0 @@ export declare class Runner { |
@@ -34,7 +34,14 @@ "use strict"; | ||
const exclude = utils_1.flatMap(options.exclude, (pattern) => normalize_glob_1.normalizeGlob(pattern, cwd)); | ||
const linterOptions = { | ||
reportUselessDirectives: options.reportUselessDirectives | ||
? options.reportUselessDirectives === true | ||
? 'error' | ||
: options.reportUselessDirectives | ||
: undefined, | ||
}; | ||
if (options.project.length === 0 && options.files.length !== 0) | ||
return this.lintFiles(Object.assign({}, options, { files, exclude }), config); | ||
return this.lintProject(Object.assign({}, options, { files, exclude }), config); | ||
return this.lintFiles(Object.assign({}, options, { files, exclude }), config, linterOptions); | ||
return this.lintProject(Object.assign({}, options, { files, exclude }), config, linterOptions); | ||
} | ||
*lintProject(options, config) { | ||
*lintProject(options, config, linterOptions) { | ||
const processorHost = new project_host_1.ProjectHost(this.directories.getCurrentDirectory(), config, this.fs, this.configManager, this.processorLoader); | ||
@@ -59,7 +66,7 @@ for (let { files, program } of this.getFilesAndProgram(options.project, options.files, options.exclude, processorHost, options.references)) { | ||
return error ? undefined : { program, file: sourceFile }; | ||
}, fix === true ? undefined : fix, program, mapped === undefined ? undefined : mapped.processor); | ||
}, fix === true ? undefined : fix, program, mapped === undefined ? undefined : mapped.processor, linterOptions); | ||
} | ||
else { | ||
summary = { | ||
findings: this.linter.getFindings(sourceFile, effectiveConfig, program, mapped === undefined ? undefined : mapped.processor), | ||
findings: this.linter.getFindings(sourceFile, effectiveConfig, program, mapped === undefined ? undefined : mapped.processor, linterOptions), | ||
fixes: 0, | ||
@@ -73,3 +80,3 @@ content: originalContent, | ||
} | ||
*lintFiles(options, config) { | ||
*lintFiles(options, config, linterOptions) { | ||
let processor; | ||
@@ -128,7 +135,7 @@ for (const file of getFiles(options.files, options.exclude, this.directories.getCurrentDirectory())) { | ||
return { file: sourceFile }; | ||
}, fix === true ? undefined : fix, undefined, processor); | ||
}, fix === true ? undefined : fix, undefined, processor, linterOptions); | ||
} | ||
else { | ||
summary = { | ||
findings: this.linter.getFindings(sourceFile, effectiveConfig, undefined, processor), | ||
findings: this.linter.getFindings(sourceFile, effectiveConfig, undefined, processor, linterOptions), | ||
fixes: 0, | ||
@@ -135,0 +142,0 @@ content: originalContent, |
@@ -9,6 +9,6 @@ import * as ts from 'typescript'; | ||
getDisabledRanges(context: FindingFilterContext): Map<string, ts.TextRange[]>; | ||
private parseLineSwitches; | ||
} | ||
export declare class DefaultLineSwitchParser implements LineSwitchParser { | ||
parse(context: LineSwitchParserContext): Map<string, RawLineSwitch[]>; | ||
private switch; | ||
parse(context: LineSwitchParserContext): RawLineSwitch[]; | ||
} |
@@ -8,3 +8,3 @@ "use strict"; | ||
const ymir_1 = require("@fimbul/ymir"); | ||
exports.LINE_SWITCH_REGEX = /^\s*wotan-(enable|disable)((?:-next)?-line)?(\s+(?:(?:[\w-]+\/)*[\w-]+\s*,\s*)*(?:[\w-]+\/)*[\w-]+)?\s*$/; | ||
exports.LINE_SWITCH_REGEX = /^ *wotan-(enable|disable)((?:-next)?-line)?( +(?:(?:[\w-]+\/)*[\w-]+ *, *)*(?:[\w-]+\/)*[\w-]+)? *$/; | ||
let LineSwitchFilterFactory = class LineSwitchFilterFactory { | ||
@@ -15,5 +15,12 @@ constructor(parser) { | ||
create(context) { | ||
return new Filter(this.getDisabledRanges(context)); | ||
const { disables, switches } = this.parseLineSwitches(context); | ||
return new Filter(disables, switches, context.sourceFile); | ||
} | ||
getDisabledRanges(context) { | ||
return new Map(Array.from(this.parseLineSwitches(context).disables, (entry) => [ | ||
entry[0], | ||
entry[1].map((range) => ({ pos: range.pos, end: range.end })), | ||
])); | ||
} | ||
parseLineSwitches(context) { | ||
const { sourceFile, ruleNames } = context; | ||
@@ -23,3 +30,2 @@ let wrappedAst; | ||
sourceFile, | ||
ruleNames, | ||
getCommentAtPosition(pos) { | ||
@@ -32,27 +38,77 @@ const wrap = tsutils_1.getWrappedNodeAtPosition(wrappedAst || (wrappedAst = context.getWrappedAst()), pos); | ||
}); | ||
const lineSwitches = []; | ||
const result = new Map(); | ||
for (const [rule, switches] of raw) { | ||
if (!ruleNames.includes(rule)) | ||
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 disables = []; | ||
let isDisabled = false; | ||
for (const s of switches.slice().sort(compareLineSwitches)) { | ||
if (s.enable) { | ||
if (!isDisabled) | ||
const rulesToSwitch = new Map(); | ||
for (const rawRuleSwitch of rawLineSwitch.rules) { | ||
const ruleSwitch = { | ||
location: rawRuleSwitch.location, | ||
fixLocation: rawRuleSwitch.fixLocation || rawRuleSwitch.location, | ||
state: 0, | ||
}; | ||
lineSwitch.rules.push(ruleSwitch); | ||
if (typeof rawRuleSwitch.predicate === 'string') { | ||
if (ruleNames.includes(rawRuleSwitch.predicate)) { | ||
if (rulesToSwitch.has(rawRuleSwitch.predicate)) { | ||
ruleSwitch.state = 2; | ||
} | ||
else { | ||
rulesToSwitch.set(rawRuleSwitch.predicate, ruleSwitch); | ||
ruleSwitch.state = 1; | ||
} | ||
} | ||
} | ||
else { | ||
const matchingNames = ruleNames.filter(makeFilterPredicate(rawRuleSwitch.predicate)); | ||
if (matchingNames.length !== 0) { | ||
ruleSwitch.state = 2; | ||
for (const rule of matchingNames) { | ||
if (!rulesToSwitch.has(rule)) { | ||
rulesToSwitch.set(rule, ruleSwitch); | ||
ruleSwitch.state = 1; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
for (const [rule, ruleSwitch] of rulesToSwitch) { | ||
const ranges = result.get(rule); | ||
if (ranges === undefined) { | ||
if (rawLineSwitch.enable) | ||
continue; | ||
isDisabled = false; | ||
disables[disables.length - 1].end = s.position; | ||
result.set(rule, [{ pos: rawLineSwitch.pos, end: rawLineSwitch.end === undefined ? Infinity : rawLineSwitch.end, switch: ruleSwitch }]); | ||
} | ||
else if (isDisabled) { | ||
continue; | ||
} | ||
else { | ||
isDisabled = true; | ||
disables.push({ pos: s.position, end: Infinity }); | ||
const last = ranges[ranges.length - 1]; | ||
if (last.end === Infinity) { | ||
if (!rawLineSwitch.enable) | ||
continue; | ||
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) { | ||
continue; | ||
} | ||
else { | ||
ranges.push({ | ||
pos: rawLineSwitch.pos, | ||
end: rawLineSwitch.end === undefined ? Infinity : rawLineSwitch.end, | ||
switch: ruleSwitch, | ||
}); | ||
} | ||
} | ||
ruleSwitch.state = 3; | ||
} | ||
if (disables.length !== 0) | ||
result.set(rule, disables); | ||
} | ||
return result; | ||
return { switches: lineSwitches, disables: result }; | ||
} | ||
@@ -65,5 +121,13 @@ }; | ||
exports.LineSwitchFilterFactory = LineSwitchFilterFactory; | ||
const stateText = { | ||
[1](singular, mode) { return `${singular ? 'is' : 'are'} already ${mode}d`; }, | ||
[0](singular) { return `do${singular ? 'es' : ''}n't match any rules enabled for this file`; }, | ||
[3](singular) { return `${singular ? 'has' : 'have'} no failures to disable`; }, | ||
[2](singular, mode) { return singular ? `was already specified in this ${mode} switch` : 'are redundant'; }, | ||
}; | ||
class Filter { | ||
constructor(disables) { | ||
constructor(disables, switches, sourceFile) { | ||
this.disables = disables; | ||
this.switches = switches; | ||
this.sourceFile = sourceFile; | ||
} | ||
@@ -74,17 +138,70 @@ filter(finding) { | ||
const { start: { position: pos }, end: { position: end } } = finding; | ||
for (const disabledRange of ruleDisables) | ||
if (end > disabledRange.pos && pos < disabledRange.end) | ||
for (const disabledRange of ruleDisables) { | ||
if (end > disabledRange.pos && pos < disabledRange.end) { | ||
disabledRange.switch.state = 4; | ||
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 + 1).fill(0); | ||
for (const rule of current.rules) | ||
++counts[rule.state]; | ||
if (counts[4] === 0 && (!current.enable || counts[3] === 0)) { | ||
const errorStates = []; | ||
for (let state = 0; state !== 4; ++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 && | ||
(!current.enable || ruleSwitch.state !== 3)) | ||
result.push(this.createFinding(`This rule ${stateText[ruleSwitch.state](true, mode)}.`, severity, ruleSwitch.location, ruleSwitch.fixLocation)); | ||
} | ||
return result; | ||
} | ||
createPosition(pos) { | ||
return Object.assign({ 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 compareLineSwitches(a, b) { | ||
return a.position - b.position || (a.enable ? 0 : 1) - (b.enable ? 0 : 1); | ||
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, ruleNames } = context; | ||
const result = new Map(); | ||
const commentRegex = /\/[/*]\s*wotan-(enable|disable)((?:-next)?-line)?(\s+(?:(?:[\w-]+\/)*[\w-]+\s*,\s*)*(?:[\w-]+\/)*[\w-]+)?\s*?(?:$|\*\/)/mg; | ||
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)) { | ||
@@ -94,12 +211,15 @@ const comment = context.getCommentAtPosition(match.index); | ||
continue; | ||
const rules = match[3] === undefined ? undefined : new Set(match[3].trim().split(/\s*,\s*/g)); | ||
const enable = match[1] === 'enable'; | ||
switch (match[2]) { | ||
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(); | ||
let { line } = ts.getLineAndCharacterOfPosition(sourceFile, comment.pos); | ||
this.switch(result, ruleNames, rules, { enable, position: lineStarts[line] }); | ||
++line; | ||
if (lineStarts.length !== line) | ||
this.switch(result, ruleNames, rules, { enable: !enable, position: lineStarts[line] }); | ||
const { line } = ts.getLineAndCharacterOfPosition(sourceFile, comment.pos); | ||
result.push({ | ||
rules, | ||
enable, | ||
pos: lineStarts[line], | ||
end: lineStarts.length === line + 1 ? undefined : lineStarts[line + 1], | ||
location: { pos: comment.pos, end: comment.end }, | ||
}); | ||
break; | ||
@@ -109,13 +229,25 @@ } | ||
const lineStarts = sourceFile.getLineStarts(); | ||
let line = ts.getLineAndCharacterOfPosition(sourceFile, comment.pos).line + 1; | ||
if (lineStarts.length === line) | ||
continue; | ||
this.switch(result, ruleNames, rules, { enable, position: lineStarts[line] }); | ||
++line; | ||
if (lineStarts.length > line) | ||
this.switch(result, ruleNames, rules, { enable: !enable, position: lineStarts[line] }); | ||
const line = ts.getLineAndCharacterOfPosition(sourceFile, comment.pos).line + 1; | ||
if (lineStarts.length === line) { | ||
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], | ||
end: lineStarts.length === line + 1 ? undefined : lineStarts[line + 1], | ||
location: { pos: comment.pos, end: comment.end }, | ||
}); | ||
} | ||
break; | ||
} | ||
default: | ||
this.switch(result, ruleNames, rules, { enable, position: comment.pos }); | ||
result.push({ rules, enable, pos: comment.pos, end: undefined, location: { pos: comment.pos, end: comment.end } }); | ||
} | ||
@@ -125,15 +257,2 @@ } | ||
} | ||
switch(map, enabled, rules = enabled, s) { | ||
for (const rule of rules) { | ||
if (!enabled.includes(rule)) | ||
continue; | ||
const existing = map.get(rule); | ||
if (existing === undefined) { | ||
map.set(rule, [s]); | ||
} | ||
else { | ||
existing.push(s); | ||
} | ||
} | ||
} | ||
}; | ||
@@ -144,2 +263,20 @@ DefaultLineSwitchParser = tslib_1.__decorate([ | ||
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 }, | ||
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; | ||
} | ||
return result; | ||
} | ||
//# sourceMappingURL=line-switches.js.map |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
322579
6.46%3980
5.88%255
1.59%+ Added
+ Added
- Removed
- Removed