Skip to content

Commit f1df7b7

Browse files
IllusionMHnchen63
authored andcommitted
Track and restore initial or previous rule state for one line lint switches (fixes palantir#1624) (palantir#1634)
1 parent 5ced33d commit f1df7b7

File tree

6 files changed

+122
-80
lines changed

6 files changed

+122
-80
lines changed

src/enableDisableRules.ts

+71-22
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,30 @@
1717

1818
import * as ts from "typescript";
1919

20+
import {AbstractRule} from "./language/rule/abstractRule";
21+
import {IOptions} from "./language/rule/rule";
2022
import {scanAllTokens} from "./language/utils";
2123
import {SkippableTokenAwareRuleWalker} from "./language/walker/skippableTokenAwareRuleWalker";
2224
import {IEnableDisablePosition} from "./ruleLoader";
2325

2426
export class EnableDisableRulesWalker extends SkippableTokenAwareRuleWalker {
2527
public enableDisableRuleMap: {[rulename: string]: IEnableDisablePosition[]} = {};
2628

29+
constructor(sourceFile: ts.SourceFile, options: IOptions, rules: {[name: string]: any}) {
30+
super(sourceFile, options);
31+
32+
if (rules) {
33+
for (const rule in rules) {
34+
if (rules.hasOwnProperty(rule) && AbstractRule.isRuleEnabled(rules[rule])) {
35+
this.enableDisableRuleMap[rule] = [{
36+
isEnabled: true,
37+
position: 0,
38+
}];
39+
}
40+
}
41+
}
42+
}
43+
2744
public visitSourceFile(node: ts.SourceFile) {
2845
super.visitSourceFile(node);
2946
const scan = ts.createScanner(ts.ScriptTarget.ES5, false, ts.LanguageVariant.Standard, node.text);
@@ -52,6 +69,29 @@ export class EnableDisableRulesWalker extends SkippableTokenAwareRuleWalker {
5269
);
5370
}
5471

72+
private switchRuleState(ruleName: string, isEnabled: boolean, start: number, end?: number): void {
73+
const ruleStateMap = this.enableDisableRuleMap[ruleName];
74+
75+
ruleStateMap.push({
76+
isEnabled,
77+
position: start,
78+
});
79+
80+
if (end) {
81+
// switchRuleState method is only called when rule state changes therefore we can safely use opposite state
82+
ruleStateMap.push({
83+
isEnabled: !isEnabled,
84+
position: end,
85+
});
86+
}
87+
}
88+
89+
private getLatestRuleState(ruleName: string): boolean {
90+
const ruleStateMap = this.enableDisableRuleMap[ruleName];
91+
92+
return ruleStateMap[ruleStateMap.length - 1].isEnabled;
93+
}
94+
5595
private handlePossibleTslintSwitch(commentText: string, startingPosition: number, node: ts.SourceFile, scanner: ts.Scanner) {
5696
// regex is: start of string followed by "/*" or "//" followed by any amount of whitespace followed by "tslint:"
5797
if (commentText.match(/^(\/\*|\/\/)\s*tslint:/)) {
@@ -82,35 +122,44 @@ export class EnableDisableRulesWalker extends SkippableTokenAwareRuleWalker {
82122
rulesList = commentTextParts[2].split(/\s+/);
83123
}
84124

85-
for (const ruleToAdd of rulesList) {
86-
if (!(ruleToAdd in this.enableDisableRuleMap)) {
87-
this.enableDisableRuleMap[ruleToAdd] = [];
125+
if (rulesList.indexOf("all") !== -1) {
126+
// iterate over all enabled rules
127+
rulesList = Object.keys(this.enableDisableRuleMap);
128+
}
129+
130+
for (const ruleToSwitch of rulesList) {
131+
if (!(ruleToSwitch in this.enableDisableRuleMap)) {
132+
// all rules enabled in configuration are already in map - skip switches for disabled rules
133+
continue;
134+
}
135+
136+
const previousState = this.getLatestRuleState(ruleToSwitch);
137+
138+
if (previousState === isEnabled) {
139+
// no need to add switch points if there is no change in rule state
140+
continue;
88141
}
142+
143+
let start: number;
144+
let end: number;
145+
89146
if (isCurrentLine) {
90147
// start at the beginning of the current line
91-
this.enableDisableRuleMap[ruleToAdd].push({
92-
isEnabled,
93-
position: this.getStartOfLinePosition(node, startingPosition),
94-
});
148+
start = this.getStartOfLinePosition(node, startingPosition);
95149
// end at the beginning of the next line
96-
this.enableDisableRuleMap[ruleToAdd].push({
97-
isEnabled: !isEnabled,
98-
position: scanner.getTextPos() + 1,
99-
});
100-
} else {
150+
end = scanner.getTextPos() + 1;
151+
} else if (isNextLine) {
101152
// start at the current position
102-
this.enableDisableRuleMap[ruleToAdd].push({
103-
isEnabled,
104-
position: startingPosition,
105-
});
153+
start = startingPosition;
106154
// end at the beginning of the line following the next line
107-
if (isNextLine) {
108-
this.enableDisableRuleMap[ruleToAdd].push({
109-
isEnabled: !isEnabled,
110-
position: this.getStartOfLinePosition(node, startingPosition, 2),
111-
});
112-
}
155+
end = this.getStartOfLinePosition(node, startingPosition, 2);
156+
} else {
157+
// disable rule for the rest of the file
158+
// start at the current position, but skip end position
159+
start = startingPosition;
113160
}
161+
162+
this.switchRuleState(ruleToSwitch, isEnabled, start, end);
114163
}
115164
}
116165
}

src/language/rule/abstractRule.ts

+13-11
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ export abstract class AbstractRule implements IRule {
2424
public static metadata: IRuleMetadata;
2525
private options: IOptions;
2626

27+
public static isRuleEnabled(ruleConfigValue: any): boolean {
28+
if (typeof ruleConfigValue === "boolean") {
29+
return ruleConfigValue;
30+
}
31+
32+
if (Array.isArray(ruleConfigValue) && ruleConfigValue.length > 0) {
33+
return ruleConfigValue[0];
34+
}
35+
36+
return false;
37+
}
38+
2739
constructor(ruleName: string, private value: any, disabledIntervals: IDisabledInterval[]) {
2840
let ruleArguments: any[] = [];
2941

@@ -50,16 +62,6 @@ export abstract class AbstractRule implements IRule {
5062
}
5163

5264
public isEnabled(): boolean {
53-
const value = this.value;
54-
55-
if (typeof value === "boolean") {
56-
return value;
57-
}
58-
59-
if (Array.isArray(value) && value.length > 0) {
60-
return value[0];
61-
}
62-
63-
return false;
65+
return AbstractRule.isRuleEnabled(this.value);
6466
}
6567
}

src/linter.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -179,19 +179,19 @@ class Linter {
179179

180180
private getEnabledRules(fileName: string, source?: string, configuration: IConfigurationFile = DEFAULT_CONFIG): IRule[] {
181181
const sourceFile = this.getSourceFile(fileName, source);
182+
const isJs = /\.jsx?$/i.test(fileName);
183+
const configurationRules = isJs ? configuration.jsRules : configuration.rules;
182184

183185
// walk the code first to find all the intervals where rules are disabled
184186
const rulesWalker = new EnableDisableRulesWalker(sourceFile, {
185187
disabledIntervals: [],
186188
ruleName: "",
187-
});
189+
}, configurationRules);
188190
rulesWalker.walk(sourceFile);
189191
const enableDisableRuleMap = rulesWalker.enableDisableRuleMap;
190192

191193
const rulesDirectories = arrayify(this.options.rulesDirectory)
192194
.concat(arrayify(configuration.rulesDirectory));
193-
const isJs = /\.jsx?$/i.test(fileName);
194-
const configurationRules = isJs ? configuration.jsRules : configuration.rules;
195195
let configuredRules = loadRules(configurationRules, enableDisableRuleMap, rulesDirectories, isJs);
196196

197197
return configuredRules.filter((r) => r.isEnabled());

src/ruleLoader.ts

+17-43
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,8 @@ export function loadRules(ruleConfiguration: {[name: string]: any},
5050
if (isJs && Rule.metadata && Rule.metadata.typescriptOnly != null && Rule.metadata.typescriptOnly) {
5151
notAllowedInJsRules.push(ruleName);
5252
} else {
53-
const all = "all"; // make the linter happy until we can turn it on and off
54-
const allList = (all in enableDisableRuleMap ? enableDisableRuleMap[all] : []);
5553
const ruleSpecificList = (ruleName in enableDisableRuleMap ? enableDisableRuleMap[ruleName] : []);
56-
const disabledIntervals = buildDisabledIntervalsFromSwitches(ruleSpecificList, allList);
54+
const disabledIntervals = buildDisabledIntervalsFromSwitches(ruleSpecificList);
5755
rules.push(new Rule(ruleName, ruleValue, disabledIntervals));
5856

5957
if (Rule.metadata && Rule.metadata.deprecationMessage && shownDeprecations.indexOf(Rule.metadata.ruleName) === -1) {
@@ -141,52 +139,28 @@ function loadRule(directory: string, ruleName: string) {
141139
return undefined;
142140
}
143141

144-
/*
145-
* We're assuming both lists are already sorted top-down so compare the tops, use the smallest of the two,
146-
* and build the intervals that way.
142+
/**
143+
* creates disabled intervals for rule based on list of switchers for it
144+
* @param ruleSpecificList - contains all switchers for rule states sorted top-down and strictly alternating between enabled and disabled
147145
*/
148-
function buildDisabledIntervalsFromSwitches(ruleSpecificList: IEnableDisablePosition[], allList: IEnableDisablePosition[]) {
149-
let isCurrentlyDisabled = false;
150-
let disabledStartPosition: number;
146+
function buildDisabledIntervalsFromSwitches(ruleSpecificList: IEnableDisablePosition[]) {
151147
const disabledIntervalList: IDisabledInterval[] = [];
152-
let i = 0;
153-
let j = 0;
154-
155-
while (i < ruleSpecificList.length || j < allList.length) {
156-
const ruleSpecificTopPositon = (i < ruleSpecificList.length ? ruleSpecificList[i].position : Infinity);
157-
const allTopPositon = (j < allList.length ? allList[j].position : Infinity);
158-
let newPositionToCheck: IEnableDisablePosition;
159-
if (ruleSpecificTopPositon < allTopPositon) {
160-
newPositionToCheck = ruleSpecificList[i];
161-
i++;
162-
} else {
163-
newPositionToCheck = allList[j];
164-
j++;
165-
}
148+
// starting from second element in the list since first is always enabled in position 0;
149+
let i = 1;
166150

167-
// we're currently disabled and enabling, or currently enabled and disabling -- a switch
168-
if (newPositionToCheck.isEnabled === isCurrentlyDisabled) {
169-
if (!isCurrentlyDisabled) {
170-
// start a new interval
171-
disabledStartPosition = newPositionToCheck.position;
172-
isCurrentlyDisabled = true;
173-
} else {
174-
// we're currently disabled and about to enable -- end the interval
175-
disabledIntervalList.push({
176-
endPosition: newPositionToCheck.position,
177-
startPosition: disabledStartPosition,
178-
});
179-
isCurrentlyDisabled = false;
180-
}
181-
}
182-
}
151+
while (i < ruleSpecificList.length) {
152+
const startPosition = ruleSpecificList[i].position;
153+
154+
// rule enabled state is always alternating therefore we can use position of next switch as end of disabled interval
155+
// set endPosition as Infinity in case when last switch for rule in a file is disabled
156+
const endPosition = ruleSpecificList[i + 1] ? ruleSpecificList[i + 1].position : Infinity;
183157

184-
if (isCurrentlyDisabled) {
185-
// we started an interval but didn't finish one -- so finish it with an Infinity
186158
disabledIntervalList.push({
187-
endPosition: Infinity,
188-
startPosition: disabledStartPosition,
159+
endPosition,
160+
startPosition,
189161
});
162+
163+
i += 2;
190164
}
191165

192166
return disabledIntervalList;

test/rules/_integration/enable-disable/test.ts.lint

+16
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,19 @@ var AAAaA = 'test'
6464

6565
/* tslint:disable:quotemark */
6666
var s = 'xxx';
67+
68+
//Test case for issue #1624
69+
// tslint:disable:quotemark variable-name
70+
var AAAaA = 'test' // tslint:disable-line:quotemark
71+
// tslint:disable-next-line:variable-name
72+
var AAAaA = 'test' //previously `quotemark` rule was enabled after previous line
73+
74+
var AAAaA = 'test' // previously both `quotemark` and `variable-name` rules were enabled after previous lines
75+
76+
// tslint:enable:quotemark
77+
// tslint:disable
78+
var AAAaA = 'test' // tslint:disable-line:quotemark
79+
var AAAaA = 'test' // ensure that disable-line rule correctly handles previous `enable rule - disable all` switches
80+
81+
// tslint:enable:no-var-keyword
82+
var AAAaA = 'test' // ensure that disabled in config rule isn't enabled
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"rules": {
33
"quotemark": [true, "double"],
4-
"variable-name": true
4+
"variable-name": true,
5+
"no-var-keyword": false
56
}
67
}

0 commit comments

Comments
 (0)