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

Commit 520f8b9

Browse files
committed
Merge branch 'master' into strict-boolean-expressions
2 parents c333e46 + 63fc794 commit 520f8b9

26 files changed

+437
-552
lines changed

src/configs/all.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ for (const key in rules) {
243243
}
244244

245245
const Rule = findRule(key, joinPaths(__dirname, "..", "rules"));
246-
if (Rule === "not-found") {
246+
if (Rule === undefined) {
247247
throw new Error(`Couldn't find rule '${key}'.`);
248248
}
249249
if (!Rule.metadata.typescriptOnly) {

src/configuration.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import * as fs from "fs";
1919
import * as path from "path";
2020
import * as resolve from "resolve";
21-
import { FatalError } from "./error";
21+
import { FatalError, showWarningOnce } from "./error";
2222

2323
import { IOptions, RuleSeverity } from "./language/rule/rule";
2424
import { arrayify, hasOwnProperty, stripComments } from "./utils";
@@ -163,9 +163,14 @@ function findup(filename: string, directory: string): string | undefined {
163163
return filename;
164164
}
165165

166+
// TODO: remove in v6.0.0
166167
// Try reading in the entire directory and looking for a file with different casing.
167168
const filenameLower = filename.toLowerCase();
168-
return fs.readdirSync(cwd).find((entry) => entry.toLowerCase() === filenameLower);
169+
const result = fs.readdirSync(cwd).find((entry) => entry.toLowerCase() === filenameLower);
170+
if (result !== undefined) {
171+
showWarningOnce(`Using mixed case tslint.json is deprecated. Found: ` + path.join(cwd, result));
172+
}
173+
return result;
169174
}
170175
}
171176

@@ -480,7 +485,7 @@ export function convertRuleOptions(ruleConfiguration: Map<string, Partial<IOptio
480485
const output: IOptions[] = [];
481486
ruleConfiguration.forEach(({ ruleArguments, ruleSeverity }, ruleName) => {
482487
const options: IOptions = {
483-
disabledIntervals: [],
488+
disabledIntervals: [], // deprecated, so just provide an empty array.
484489
ruleArguments: ruleArguments != null ? ruleArguments : [],
485490
ruleName,
486491
ruleSeverity: ruleSeverity != null ? ruleSeverity : "error",

src/enableDisableRules.ts

Lines changed: 118 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -15,133 +15,144 @@
1515
* limitations under the License.
1616
*/
1717

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

2120
import * as utils from "tsutils";
2221
import * as ts from "typescript";
23-
import { IOptions } from "./language/rule/rule";
24-
25-
import { IEnableDisablePosition } from "./ruleLoader";
26-
27-
export class EnableDisableRulesWalker {
28-
private enableDisableRuleMap: Map<string, IEnableDisablePosition[]>;
29-
private enabledRules: string[];
30-
31-
constructor(private sourceFile: ts.SourceFile, ruleOptionsList: IOptions[]) {
32-
this.enableDisableRuleMap = new Map<string, IEnableDisablePosition[]>();
33-
this.enabledRules = [];
34-
for (const ruleOptions of ruleOptionsList) {
35-
if (ruleOptions.ruleSeverity !== "off") {
36-
this.enabledRules.push(ruleOptions.ruleName);
37-
this.enableDisableRuleMap.set(ruleOptions.ruleName, [{
38-
isEnabled: true,
39-
position: 0,
40-
}]);
41-
}
42-
}
43-
}
44-
45-
public getEnableDisableRuleMap() {
46-
utils.forEachComment(this.sourceFile, (fullText, comment) => {
47-
const commentText = comment.kind === ts.SyntaxKind.SingleLineCommentTrivia
48-
? fullText.substring(comment.pos + 2, comment.end)
49-
: fullText.substring(comment.pos + 2, comment.end - 2);
50-
return this.handleComment(commentText, comment);
51-
});
52-
53-
return this.enableDisableRuleMap;
54-
}
22+
import { RuleFailure } from "./language/rule/rule";
5523

56-
private getStartOfLinePosition(position: number, lineOffset = 0) {
57-
const line = ts.getLineAndCharacterOfPosition(this.sourceFile, position).line + lineOffset;
58-
const lineStarts = this.sourceFile.getLineStarts();
59-
if (line >= lineStarts.length) {
60-
// next line ends with eof or there is no next line
61-
// undefined switches the rule until the end and avoids an extra array entry
62-
return undefined;
63-
}
64-
return lineStarts[line];
24+
export function removeDisabledFailures(sourceFile: ts.SourceFile, failures: RuleFailure[]): RuleFailure[] {
25+
if (failures.length === 0) {
26+
// Usually there won't be failures anyway, so no need to look for "tslint:disable".
27+
return failures;
6528
}
6629

67-
private switchRuleState(ruleName: string, isEnabled: boolean, start: number, end?: number): void {
68-
const ruleStateMap = this.enableDisableRuleMap.get(ruleName);
69-
if (ruleStateMap === undefined || // skip switches for unknown or disabled rules
70-
isEnabled === ruleStateMap[ruleStateMap.length - 1].isEnabled // no need to add switch points if there is no change
71-
) {
72-
return;
73-
}
74-
75-
ruleStateMap.push({
76-
isEnabled,
77-
position: start,
30+
const failingRules = new Set(failures.map((f) => f.getRuleName()));
31+
const map = getDisableMap(sourceFile, failingRules);
32+
return failures.filter((failure) => {
33+
const disabledIntervals = map.get(failure.getRuleName());
34+
return disabledIntervals === undefined || !disabledIntervals.some(({ pos, end }) => {
35+
const failPos = failure.getStartPosition().getPosition();
36+
const failEnd = failure.getEndPosition().getPosition();
37+
return failEnd >= pos && (end === -1 || failPos <= end);
7838
});
39+
});
40+
}
7941

80-
if (end !== undefined) {
81-
// we only get here when rule state changes therefore we can safely use opposite state
82-
ruleStateMap.push({
83-
isEnabled: !isEnabled,
84-
position: end,
85-
});
42+
/**
43+
* The map will have an array of TextRange for each disable of a rule in a file.
44+
* (It will have no entry if the rule is never disabled, meaning all arrays are non-empty.)
45+
*/
46+
function getDisableMap(sourceFile: ts.SourceFile, failingRules: Set<string>): ReadonlyMap<string, ts.TextRange[]> {
47+
const map = new Map<string, ts.TextRange[]>();
48+
49+
utils.forEachComment(sourceFile, (fullText, comment) => {
50+
const commentText = comment.kind === ts.SyntaxKind.SingleLineCommentTrivia
51+
? fullText.substring(comment.pos + 2, comment.end)
52+
: fullText.substring(comment.pos + 2, comment.end - 2);
53+
const parsed = parseComment(commentText);
54+
if (parsed !== undefined) {
55+
const { rulesList, isEnabled, modifier } = parsed;
56+
const switchRange = getSwitchRange(modifier, comment, sourceFile);
57+
if (switchRange !== undefined) {
58+
const rulesToSwitch = rulesList === "all" ? Array.from(failingRules) : rulesList.filter((r) => failingRules.has(r));
59+
for (const ruleToSwitch of rulesToSwitch) {
60+
switchRuleState(ruleToSwitch, isEnabled, switchRange.pos, switchRange.end);
61+
}
62+
}
8663
}
87-
}
88-
89-
private handleComment(commentText: string, range: ts.TextRange) {
90-
// regex is: start of string followed by any amount of whitespace
91-
// followed by tslint and colon
92-
// followed by either "enable" or "disable"
93-
// followed optionally by -line or -next-line
94-
// followed by either colon, whitespace or end of string
95-
const match = /^\s*tslint:(enable|disable)(?:-(line|next-line))?(:|\s|$)/.exec(commentText);
96-
if (match !== null) {
97-
// remove everything matched by the previous regex to get only the specified rules
98-
// split at whitespaces
99-
// filter empty items coming from whitespaces at start, at end or empty list
100-
let rulesList = commentText.substr(match[0].length)
101-
.split(/\s+/)
102-
.filter((rule) => rule !== "");
103-
if (rulesList.length === 0 && match[3] === ":") {
104-
// nothing to do here: an explicit separator was specified but no rules to switch
105-
return;
64+
});
65+
66+
return map;
67+
68+
function switchRuleState(ruleName: string, isEnable: boolean, start: number, end: number): void {
69+
const disableRanges = map.get(ruleName);
70+
71+
if (isEnable) {
72+
if (disableRanges !== undefined) {
73+
const lastDisable = disableRanges[disableRanges.length - 1];
74+
if (lastDisable.end === -1) {
75+
lastDisable.end = start;
76+
if (end !== -1) {
77+
// Disable it again after the enable range is over.
78+
disableRanges.push({ pos: end, end: -1 });
79+
}
80+
}
10681
}
107-
if (rulesList.length === 0 ||
108-
rulesList.indexOf("all") !== -1) {
109-
// if list is empty we default to all enabled rules
110-
// if `all` is specified we ignore the other rules and take all enabled rules
111-
rulesList = this.enabledRules;
82+
} else { // disable
83+
if (disableRanges === undefined) {
84+
map.set(ruleName, [{ pos: start, end }]);
85+
} else if (disableRanges[disableRanges.length - 1].end !== -1) {
86+
disableRanges.push({ pos: start, end });
11287
}
113-
114-
this.handleTslintLineSwitch(rulesList, match[1] === "enable", match[2], range);
11588
}
11689
}
90+
}
11791

118-
private handleTslintLineSwitch(rules: string[], isEnabled: boolean, modifier: string, range: ts.TextRange) {
119-
let start: number | undefined;
120-
let end: number | undefined;
121-
122-
if (modifier === "line") {
123-
// start at the beginning of the line where comment starts
124-
start = this.getStartOfLinePosition(range.pos)!;
125-
// end at the beginning of the line following the comment
126-
end = this.getStartOfLinePosition(range.end, 1);
127-
} else if (modifier === "next-line") {
92+
/** End will be -1 to indicate no end. */
93+
function getSwitchRange(modifier: Modifier, range: ts.TextRange, sourceFile: ts.SourceFile): ts.TextRange | undefined {
94+
const lineStarts = sourceFile.getLineStarts();
95+
96+
switch (modifier) {
97+
case "line":
98+
return {
99+
// start at the beginning of the line where comment starts
100+
pos: getStartOfLinePosition(range.pos),
101+
// end at the beginning of the line following the comment
102+
end: getStartOfLinePosition(range.end, 1),
103+
};
104+
case "next-line":
128105
// start at the beginning of the line following the comment
129-
start = this.getStartOfLinePosition(range.end, 1);
130-
if (start === undefined) {
106+
const pos = getStartOfLinePosition(range.end, 1);
107+
if (pos === -1) {
131108
// no need to switch anything, there is no next line
132-
return;
109+
return undefined;
133110
}
134111
// end at the beginning of the line following the next line
135-
end = this.getStartOfLinePosition(range.end, 2);
136-
} else {
112+
return { pos, end: getStartOfLinePosition(range.end, 2) };
113+
default:
137114
// switch rule for the rest of the file
138115
// start at the current position, but skip end position
139-
start = range.pos;
140-
end = undefined;
141-
}
116+
return { pos: range.pos, end: -1 };
117+
}
142118

143-
for (const ruleToSwitch of rules) {
144-
this.switchRuleState(ruleToSwitch, isEnabled, start, end);
145-
}
119+
/** Returns -1 for last line. */
120+
function getStartOfLinePosition(position: number, lineOffset = 0): number {
121+
const line = ts.getLineAndCharacterOfPosition(sourceFile, position).line + lineOffset;
122+
return line >= lineStarts.length ? -1 : lineStarts[line];
146123
}
147124
}
125+
126+
type Modifier = "line" | "next-line" | undefined;
127+
function parseComment(commentText: string): { rulesList: string[] | "all", isEnabled: boolean, modifier: Modifier } | undefined {
128+
// regex is: start of string followed by any amount of whitespace
129+
// followed by tslint and colon
130+
// followed by either "enable" or "disable"
131+
// followed optionally by -line or -next-line
132+
// followed by either colon, whitespace or end of string
133+
const match = /^\s*tslint:(enable|disable)(?:-(line|next-line))?(:|\s|$)/.exec(commentText);
134+
if (match === null) {
135+
return undefined;
136+
}
137+
138+
// remove everything matched by the previous regex to get only the specified rules
139+
// split at whitespaces
140+
// filter empty items coming from whitespaces at start, at end or empty list
141+
let rulesList: string[] | "all" = splitOnSpaces(commentText.substr(match[0].length));
142+
if (rulesList.length === 0 && match[3] === ":") {
143+
// nothing to do here: an explicit separator was specified but no rules to switch
144+
return undefined;
145+
}
146+
if (rulesList.length === 0 ||
147+
rulesList.indexOf("all") !== -1) {
148+
// if list is empty we default to all enabled rules
149+
// if `all` is specified we ignore the other rules and take all enabled rules
150+
rulesList = "all";
151+
}
152+
153+
return { rulesList, isEnabled: match[1] === "enable", modifier: match[2] as Modifier };
154+
}
155+
156+
function splitOnSpaces(str: string): string[] {
157+
return str.split(/\s+/).filter((s) => s !== "");
158+
}

src/language/rule/abstractRule.ts

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
import * as ts from "typescript";
1919

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

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

4241
public applyWithWalker(walker: IWalker): RuleFailure[] {
4342
walker.walk(walker.getSourceFile());
44-
return this.filterFailures(walker.getFailures());
43+
return walker.getFailures();
4544
}
4645

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

67-
protected filterFailures(failures: RuleFailure[]): RuleFailure[] {
68-
const result: RuleFailure[] = [];
69-
for (const failure of failures) {
70-
// don't add failures for a rule if the failure intersects an interval where that rule is disabled
71-
if (!doesIntersect(failure, this.options.disabledIntervals) && !result.some((f) => f.equals(failure))) {
72-
result.push(failure);
73-
}
74-
}
75-
return result;
76-
}
66+
/**
67+
* @deprecated
68+
* Failures will be filtered based on `tslint:disable` comments by tslint.
69+
* This method now does nothing.
70+
*/
71+
protected filterFailures(failures: RuleFailure[]) { return failures; }
7772
}

src/language/rule/rule.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,18 @@ export interface IOptions {
9999
ruleArguments: any[];
100100
ruleSeverity: RuleSeverity;
101101
ruleName: string;
102-
disabledIntervals: IDisabledInterval[];
102+
/**
103+
* @deprecated
104+
* Tslint now handles disables itself.
105+
* This will be empty.
106+
*/
107+
disabledIntervals: IDisabledInterval[]; // tslint:disable-line deprecation
103108
}
104109

110+
/**
111+
* @deprecated
112+
* These are now handled internally.
113+
*/
105114
export interface IDisabledInterval {
106115
startPosition: number;
107116
endPosition: number;

src/language/utils.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,15 @@ import * as path from "path";
1919
import { isBlockScopedVariableDeclarationList } from "tsutils";
2020
import * as ts from "typescript";
2121

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

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

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

0 commit comments

Comments
 (0)