|
15 | 15 | * limitations under the License.
|
16 | 16 | */
|
17 | 17 |
|
18 |
| -// tslint:disable prefer-switch |
19 |
| -// (waiting on https://github.com/palantir/tslint/pull/2369) |
| 18 | +// tslint:disable object-literal-sort-keys |
20 | 19 |
|
21 | 20 | import * as utils from "tsutils";
|
22 | 21 | import * as ts from "typescript";
|
23 |
| -import { IOptions } from "./language/rule/rule"; |
| 22 | +import { RuleFailure } from "./language/rule/rule"; |
24 | 23 |
|
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; |
| 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; |
54 | 28 | }
|
55 | 29 |
|
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]; |
65 |
| - } |
66 |
| - |
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); |
78 | 38 | });
|
| 39 | + }); |
| 40 | +} |
79 | 41 |
|
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 | + } |
86 | 63 | }
|
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 | + } |
106 | 81 | }
|
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 }); |
112 | 87 | }
|
113 |
| - |
114 |
| - this.handleTslintLineSwitch(rulesList, match[1] === "enable", match[2], range); |
115 | 88 | }
|
116 | 89 | }
|
| 90 | +} |
117 | 91 |
|
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": |
128 | 105 | // 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) { |
131 | 108 | // no need to switch anything, there is no next line
|
132 |
| - return; |
| 109 | + return undefined; |
133 | 110 | }
|
134 | 111 | // 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: |
137 | 114 | // switch rule for the rest of the file
|
138 | 115 | // 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 | + } |
142 | 118 |
|
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]; |
| 123 | + } |
| 124 | +} |
| 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; |
146 | 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" = commentText.substr(match[0].length) |
| 142 | + .split(/\s+/) |
| 143 | + .filter((rule) => rule !== undefined); |
| 144 | + if (rulesList.length === 0 && match[3] === ":") { |
| 145 | + // nothing to do here: an explicit separator was specified but no rules to switch |
| 146 | + return undefined; |
| 147 | + } |
| 148 | + if (rulesList.length === 0 || |
| 149 | + rulesList.indexOf("all") !== -1) { |
| 150 | + // if list is empty we default to all enabled rules |
| 151 | + // if `all` is specified we ignore the other rules and take all enabled rules |
| 152 | + rulesList = "all"; |
| 153 | + } |
| 154 | + |
| 155 | + return { rulesList, isEnabled: match[1] === "enable", modifier: match[2] as Modifier }; |
147 | 156 | }
|
0 commit comments