Skip to content
This repository has been archived by the owner on Mar 25, 2021. It is now read-only.

Rewrite enable/disable logic #2369

Merged
merged 18 commits into from
May 9, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/configs/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ for (const key in rules) {
}

const Rule = findRule(key, joinPaths(__dirname, "..", "rules"));
if (Rule === "not-found") {
if (Rule === undefined) {
throw new Error(`Couldn't find rule '${key}'.`);
}
if (!Rule.metadata.typescriptOnly) {
Expand Down
2 changes: 1 addition & 1 deletion src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ export function convertRuleOptions(ruleConfiguration: Map<string, Partial<IOptio
const output: IOptions[] = [];
ruleConfiguration.forEach((partialOptions, ruleName) => {
const options: IOptions = {
disabledIntervals: [],
disabledIntervals: [], // deprecated, so just provide an empty array.
ruleArguments: partialOptions.ruleArguments || [],
ruleName,
ruleSeverity: partialOptions.ruleSeverity || "error",
Expand Down
223 changes: 116 additions & 107 deletions src/enableDisableRules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,133 +15,142 @@
* limitations under the License.
*/

// tslint:disable prefer-switch
// (waiting on https://github.com/palantir/tslint/pull/2369)
// tslint:disable object-literal-sort-keys

import * as utils from "tsutils";
import * as ts from "typescript";
import { IOptions } from "./language/rule/rule";
import { RuleFailure } from "./language/rule/rule";

import { IEnableDisablePosition } from "./ruleLoader";

export class EnableDisableRulesWalker {
private enableDisableRuleMap: Map<string, IEnableDisablePosition[]>;
private enabledRules: string[];

constructor(private sourceFile: ts.SourceFile, ruleOptionsList: IOptions[]) {
this.enableDisableRuleMap = new Map<string, IEnableDisablePosition[]>();
this.enabledRules = [];
for (const ruleOptions of ruleOptionsList) {
if (ruleOptions.ruleSeverity !== "off") {
this.enabledRules.push(ruleOptions.ruleName);
this.enableDisableRuleMap.set(ruleOptions.ruleName, [{
isEnabled: true,
position: 0,
}]);
}
}
}

public getEnableDisableRuleMap() {
utils.forEachComment(this.sourceFile, (fullText, comment) => {
const commentText = comment.kind === ts.SyntaxKind.SingleLineCommentTrivia
? fullText.substring(comment.pos + 2, comment.end)
: fullText.substring(comment.pos + 2, comment.end - 2);
return this.handleComment(commentText, comment);
});

return this.enableDisableRuleMap;
export function removeDisabledFailures(sourceFile: ts.SourceFile, failures: RuleFailure[]): RuleFailure[] {
if (!failures.length) {
// Usually there won't be failures anyway, so no need to look for "tslint:disable".
return failures;
}

private getStartOfLinePosition(position: number, lineOffset = 0) {
const line = ts.getLineAndCharacterOfPosition(this.sourceFile, position).line + lineOffset;
const lineStarts = this.sourceFile.getLineStarts();
if (line >= lineStarts.length) {
// next line ends with eof or there is no next line
// undefined switches the rule until the end and avoids an extra array entry
return undefined;
}
return lineStarts[line];
}

private switchRuleState(ruleName: string, isEnabled: boolean, start: number, end?: number): void {
const ruleStateMap = this.enableDisableRuleMap.get(ruleName);
if (ruleStateMap === undefined || // skip switches for unknown or disabled rules
isEnabled === ruleStateMap[ruleStateMap.length - 1].isEnabled // no need to add switch points if there is no change
) {
return;
}

ruleStateMap.push({
isEnabled,
position: start,
const failingRules = new Set(failures.map((f) => f.getRuleName()));
const map = getDisableMap(sourceFile, failingRules);
return failures.filter((failure) => {
const disabledIntervals = map.get(failure.getRuleName());
return !disabledIntervals || !disabledIntervals.some(({ pos, end }) => {
const failPos = failure.getStartPosition().getPosition();
const failEnd = failure.getEndPosition().getPosition();
return failEnd >= pos && (end === -1 || failPos <= end);
});
});
}

if (end) {
// we only get here when rule state changes therefore we can safely use opposite state
ruleStateMap.push({
isEnabled: !isEnabled,
position: end,
});
/**
* The map will have an array of TextRange for each disable of a rule in a file.
* (It will have no entry if the rule is never disabled, meaning all arrays are non-empty.)
*/
function getDisableMap(sourceFile: ts.SourceFile, failingRules: Set<string>): ReadonlyMap<string, ts.TextRange[]> {
const map = new Map<string, ts.TextRange[]>();

utils.forEachComment(sourceFile, (fullText, comment) => {
const commentText = comment.kind === ts.SyntaxKind.SingleLineCommentTrivia
? fullText.substring(comment.pos + 2, comment.end)
: fullText.substring(comment.pos + 2, comment.end - 2);
const parsed = parseComment(commentText);
if (parsed) {
const { rulesList, isEnabled, modifier } = parsed;
const switchRange = getSwitchRange(modifier, comment, sourceFile);
if (switchRange) {
const rulesToSwitch = rulesList === "all" ? Array.from(failingRules) : rulesList.filter((r) => failingRules.has(r));
for (const ruleToSwitch of rulesToSwitch) {
switchRuleState(ruleToSwitch, isEnabled, switchRange.pos, switchRange.end);
}
}
}
}

private handleComment(commentText: string, range: ts.TextRange) {
// regex is: start of string followed by any amount of whitespace
// followed by tslint and colon
// followed by either "enable" or "disable"
// followed optionally by -line or -next-line
// followed by either colon, whitespace or end of string
const match = /^\s*tslint:(enable|disable)(?:-(line|next-line))?(:|\s|$)/.exec(commentText);
if (match !== null) {
// remove everything matched by the previous regex to get only the specified rules
// split at whitespaces
// filter empty items coming from whitespaces at start, at end or empty list
let rulesList = commentText.substr(match[0].length)
.split(/\s+/)
.filter((rule) => !!rule);
if (rulesList.length === 0 && match[3] === ":") {
// nothing to do here: an explicit separator was specified but no rules to switch
return;
});

return map;

function switchRuleState(ruleName: string, isEnable: boolean, start: number, end: number): void {
const disableRanges = map.get(ruleName);

if (isEnable) {
if (disableRanges) {
const lastDisable = disableRanges[disableRanges.length - 1];
if (lastDisable.end === -1) {
lastDisable.end = start;
if (end !== -1) {
// Disable it again after the enable range is over.
disableRanges.push({ pos: end, end: -1 });
}
}
}
if (rulesList.length === 0 ||
rulesList.indexOf("all") !== -1) {
// if list is empty we default to all enabled rules
// if `all` is specified we ignore the other rules and take all enabled rules
rulesList = this.enabledRules;
} else { // disable
if (!disableRanges) {
map.set(ruleName, [{ pos: start, end }]);
} else if (disableRanges[disableRanges.length - 1].end !== -1) {
disableRanges.push({ pos: start, end });
}

this.handleTslintLineSwitch(rulesList, match[1] === "enable", match[2], range);
}
}
}

private handleTslintLineSwitch(rules: string[], isEnabled: boolean, modifier: string, range: ts.TextRange) {
let start: number | undefined;
let end: number | undefined;

if (modifier === "line") {
// start at the beginning of the line where comment starts
start = this.getStartOfLinePosition(range.pos)!;
// end at the beginning of the line following the comment
end = this.getStartOfLinePosition(range.end, 1);
} else if (modifier === "next-line") {
/** End will be -1 to indicate no end. */
function getSwitchRange(modifier: Modifier, range: ts.TextRange, sourceFile: ts.SourceFile): ts.TextRange | undefined {
const lineStarts = sourceFile.getLineStarts();

switch (modifier) {
case "line":
return {
// start at the beginning of the line where comment starts
pos: getStartOfLinePosition(range.pos),
// end at the beginning of the line following the comment
end: getStartOfLinePosition(range.end, 1),
};
case "next-line":
// start at the beginning of the line following the comment
start = this.getStartOfLinePosition(range.end, 1);
if (start === undefined) {
const pos = getStartOfLinePosition(range.end, 1);
if (pos === -1) {
// no need to switch anything, there is no next line
return;
return undefined;
}
// end at the beginning of the line following the next line
end = this.getStartOfLinePosition(range.end, 2);
} else {
return { pos, end: getStartOfLinePosition(range.end, 2) };
default:
// switch rule for the rest of the file
// start at the current position, but skip end position
start = range.pos;
end = undefined;
}
return { pos: range.pos, end: -1 };
}

for (const ruleToSwitch of rules) {
this.switchRuleState(ruleToSwitch, isEnabled, start, end);
}
/** Returns -1 for last line. */
function getStartOfLinePosition(position: number, lineOffset = 0): number {
const line = ts.getLineAndCharacterOfPosition(sourceFile, position).line + lineOffset;
return line >= lineStarts.length ? -1 : lineStarts[line];
}
}

type Modifier = "line" | "next-line" | undefined;
function parseComment(commentText: string): { rulesList: string[] | "all", isEnabled: boolean, modifier: Modifier } | undefined {
// regex is: start of string followed by any amount of whitespace
// followed by tslint and colon
// followed by either "enable" or "disable"
// followed optionally by -line or -next-line
// followed by either colon, whitespace or end of string
const match = /^\s*tslint:(enable|disable)(?:-(line|next-line))?(:|\s|$)/.exec(commentText);
if (match === null) {
return undefined;
}

// remove everything matched by the previous regex to get only the specified rules
// split at whitespaces
// filter empty items coming from whitespaces at start, at end or empty list
let rulesList: string[] | "all" = commentText.substr(match[0].length)
.split(/\s+/)
.filter((rule) => !!rule);
if (rulesList.length === 0 && match[3] === ":") {
// nothing to do here: an explicit separator was specified but no rules to switch
return undefined;
}
if (rulesList.length === 0 ||
rulesList.indexOf("all") !== -1) {
// if list is empty we default to all enabled rules
// if `all` is specified we ignore the other rules and take all enabled rules
rulesList = "all";
}

return { rulesList, isEnabled: match[1] === "enable", modifier: match[2] as Modifier };
}
21 changes: 8 additions & 13 deletions src/language/rule/abstractRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

import * as ts from "typescript";

import {doesIntersect} from "../utils";
import {IWalker, WalkContext} from "../walker";
import { IOptions, IRule, IRuleMetadata, RuleFailure, RuleSeverity } from "./rule";

Expand All @@ -41,7 +40,7 @@ export abstract class AbstractRule implements IRule {

public applyWithWalker(walker: IWalker): RuleFailure[] {
walker.walk(walker.getSourceFile());
return this.filterFailures(walker.getFailures());
return walker.getFailures();
}

public isEnabled(): boolean {
Expand All @@ -61,17 +60,13 @@ export abstract class AbstractRule implements IRule {
): RuleFailure[] {
const ctx = new WalkContext(sourceFile, this.ruleName, options);
walkFn(ctx);
return this.filterFailures(ctx.failures);
return ctx.failures;
}

protected filterFailures(failures: RuleFailure[]): RuleFailure[] {
const result: RuleFailure[] = [];
for (const failure of failures) {
// don't add failures for a rule if the failure intersects an interval where that rule is disabled
if (!doesIntersect(failure, this.options.disabledIntervals) && !result.some((f) => f.equals(failure))) {
result.push(failure);
}
}
return result;
}
/**
* @deprecated
* Failures will be filtered based on `tslint:disable` comments by tslint.
* This method now does nothing.
*/
protected filterFailures(failures: RuleFailure[]) { return failures; }
}
11 changes: 10 additions & 1 deletion src/language/rule/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,18 @@ export interface IOptions {
ruleArguments: any[];
ruleSeverity: RuleSeverity;
ruleName: string;
disabledIntervals: IDisabledInterval[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be kept and deprecated for backwards compatibility

/**
* @deprecated
* Tslint now handles disables itself.
* This will be empty.
*/
disabledIntervals: IDisabledInterval[]; // tslint:disable-line deprecation
}

/**
* @deprecated
* These are now handled internally.
*/
export interface IDisabledInterval {
startPosition: number;
endPosition: number;
Expand Down
5 changes: 3 additions & 2 deletions src/language/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,15 @@ import * as path from "path";
import { isBlockScopedVariableDeclarationList } from "tsutils";
import * as ts from "typescript";

import {IDisabledInterval, RuleFailure} from "./rule/rule";
import {IDisabledInterval, RuleFailure} from "./rule/rule"; // tslint:disable-line deprecation

export function getSourceFile(fileName: string, source: string): ts.SourceFile {
const normalizedName = path.normalize(fileName).replace(/\\/g, "/");
return ts.createSourceFile(normalizedName, source, ts.ScriptTarget.ES5, /*setParentNodes*/ true);
}

export function doesIntersect(failure: RuleFailure, disabledIntervals: IDisabledInterval[]) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another breaking change

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is public. (#2446)

/** @deprecated See IDisabledInterval. */
export function doesIntersect(failure: RuleFailure, disabledIntervals: IDisabledInterval[]): boolean { // tslint:disable-line deprecation
return disabledIntervals.some((interval) => {
const maxStart = Math.max(interval.startPosition, failure.getStartPosition().getPosition());
const minEnd = Math.min(interval.endPosition, failure.getEndPosition().getPosition());
Expand Down
Loading