This repository has been archived by the owner on Mar 25, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 887
Rewrite and fix EnableDisableRulesWalker #2062
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,20 +18,20 @@ | |
import * as ts from "typescript"; | ||
|
||
import {AbstractRule} from "./language/rule/abstractRule"; | ||
import {IOptions} from "./language/rule/rule"; | ||
import {forEachComment, TokenPosition} from "./language/utils"; | ||
import {RuleWalker} from "./language/walker/ruleWalker"; | ||
import {IEnableDisablePosition} from "./ruleLoader"; | ||
|
||
export class EnableDisableRulesWalker extends RuleWalker { | ||
public enableDisableRuleMap: {[rulename: string]: IEnableDisablePosition[]} = {}; | ||
|
||
constructor(sourceFile: ts.SourceFile, options: IOptions, rules: {[name: string]: any}) { | ||
super(sourceFile, options); | ||
export class EnableDisableRulesWalker { | ||
private enableDisableRuleMap: {[rulename: string]: IEnableDisablePosition[]}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wanted to change that, but there are many exported functions that expect this to be an object. So this would require a larger rewrite. |
||
private enabledRules: string[]; | ||
|
||
constructor(private sourceFile: ts.SourceFile, rules: {[name: string]: any}) { | ||
this.enableDisableRuleMap = {}; | ||
this.enabledRules = []; | ||
if (rules) { | ||
for (const rule in rules) { | ||
if (rules.hasOwnProperty(rule) && AbstractRule.isRuleEnabled(rules[rule])) { | ||
for (const rule of Object.keys(rules)) { | ||
if (AbstractRule.isRuleEnabled(rules[rule])) { | ||
this.enabledRules.push(rule); | ||
this.enableDisableRuleMap[rule] = [{ | ||
isEnabled: true, | ||
position: 0, | ||
|
@@ -41,116 +41,106 @@ export class EnableDisableRulesWalker extends RuleWalker { | |
} | ||
} | ||
|
||
public visitSourceFile(node: ts.SourceFile) { | ||
forEachComment(node, (fullText, _kind, pos) => { | ||
return this.handlePossibleTslintSwitch(fullText.substring(pos.tokenStart, pos.end), node, pos); | ||
public getEnableDisableRuleMap() { | ||
forEachComment(this.sourceFile, (fullText, kind, pos) => { | ||
const commentText = kind === ts.SyntaxKind.SingleLineCommentTrivia | ||
? fullText.substring(pos.tokenStart + 2, pos.end) | ||
: fullText.substring(pos.tokenStart + 2, pos.end - 2); | ||
return this.handleComment(commentText, pos); | ||
}); | ||
|
||
return this.enableDisableRuleMap; | ||
} | ||
|
||
private getStartOfLinePosition(node: ts.SourceFile, position: number, lineOffset = 0) { | ||
const line = ts.getLineAndCharacterOfPosition(node, position).line + lineOffset; | ||
const lineStarts = node.getLineStarts(); | ||
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 | ||
return node.getFullWidth(); | ||
// 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[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, | ||
}); | ||
|
||
if (end) { | ||
// switchRuleState method is only called when rule state changes therefore we can safely use opposite state | ||
// we only get here when rule state changes therefore we can safely use opposite state | ||
ruleStateMap.push({ | ||
isEnabled: !isEnabled, | ||
position: end, | ||
}); | ||
} | ||
} | ||
|
||
private getLatestRuleState(ruleName: string): boolean { | ||
const ruleStateMap = this.enableDisableRuleMap[ruleName]; | ||
private handleComment(commentText: string, pos: TokenPosition) { | ||
// 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; | ||
} | ||
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; | ||
} | ||
|
||
return ruleStateMap[ruleStateMap.length - 1].isEnabled; | ||
this.handleTslintLineSwitch(rulesList, match[1] === "enable", match[2], pos); | ||
} | ||
} | ||
|
||
private handlePossibleTslintSwitch(commentText: string, node: ts.SourceFile, pos: TokenPosition) { | ||
// regex is: start of string followed by "/*" or "//" followed by any amount of whitespace followed by "tslint:" | ||
if (commentText.match(/^(\/\*|\/\/)\s*tslint:/)) { | ||
const commentTextParts = commentText.split(":"); | ||
// regex is: start of string followed by either "enable" or "disable" | ||
// followed optionally by -line or -next-line | ||
// followed by either whitespace or end of string | ||
const enableOrDisableMatch = commentTextParts[1].match(/^(enable|disable)(-(line|next-line))?(\s|$)/); | ||
|
||
if (enableOrDisableMatch != null) { | ||
const isEnabled = enableOrDisableMatch[1] === "enable"; | ||
const isCurrentLine = enableOrDisableMatch[3] === "line"; | ||
const isNextLine = enableOrDisableMatch[3] === "next-line"; | ||
|
||
let rulesList = ["all"]; | ||
|
||
if (commentTextParts.length === 2) { | ||
// an implicit whitespace separator is used for the rules list. | ||
rulesList = commentTextParts[1].split(/\s+/).slice(1); | ||
|
||
// remove empty items and potential comment end. | ||
rulesList = rulesList.filter((item) => !!item && !item.includes("*/")); | ||
|
||
// potentially there were no items, so default to `all`. | ||
rulesList = rulesList.length > 0 ? rulesList : ["all"]; | ||
} else if (commentTextParts.length > 2) { | ||
// an explicit separator was specified for the rules list. | ||
rulesList = commentTextParts[2].split(/\s+/); | ||
} | ||
|
||
if (rulesList.indexOf("all") !== -1) { | ||
// iterate over all enabled rules | ||
rulesList = Object.keys(this.enableDisableRuleMap); | ||
} | ||
|
||
for (const ruleToSwitch of rulesList) { | ||
if (!(ruleToSwitch in this.enableDisableRuleMap)) { | ||
// all rules enabled in configuration are already in map - skip switches for disabled rules | ||
continue; | ||
} | ||
|
||
const previousState = this.getLatestRuleState(ruleToSwitch); | ||
|
||
if (previousState === isEnabled) { | ||
// no need to add switch points if there is no change in rule state | ||
continue; | ||
} | ||
|
||
let start: number; | ||
let end: number | undefined; | ||
|
||
if (isCurrentLine) { | ||
// start at the beginning of the current line | ||
start = this.getStartOfLinePosition(node, pos.tokenStart); | ||
// end at the beginning of the next line | ||
end = pos.end + 1; | ||
} else if (isNextLine) { | ||
// start at the current position | ||
start = pos.tokenStart; | ||
// end at the beginning of the line following the next line | ||
end = this.getStartOfLinePosition(node, pos.tokenStart, 2); | ||
} else { | ||
// disable rule for the rest of the file | ||
// start at the current position, but skip end position | ||
start = pos.tokenStart; | ||
end = undefined; | ||
} | ||
|
||
this.switchRuleState(ruleToSwitch, isEnabled, start, end); | ||
} | ||
private handleTslintLineSwitch(rules: string[], isEnabled: boolean, modifier: string, pos: TokenPosition) { | ||
let start: number | undefined; | ||
let end: number | undefined; | ||
|
||
if (modifier === "line") { | ||
// start at the beginning of the line where comment starts | ||
start = this.getStartOfLinePosition(pos.tokenStart)!; | ||
// end at the beginning of the line following the comment | ||
end = this.getStartOfLinePosition(pos.end, 1); | ||
} else if (modifier === "next-line") { | ||
// start at the beginning of the line following the comment | ||
start = this.getStartOfLinePosition(pos.end, 1); | ||
if (start === undefined) { | ||
// no need to switch anything, there is no next line | ||
return; | ||
} | ||
// end at the beginning of the line following the next line | ||
end = this.getStartOfLinePosition(pos.end, 2); | ||
} else { | ||
// switch rule for the rest of the file | ||
// start at the current position, but skip end position | ||
start = pos.tokenStart; | ||
end = undefined; | ||
} | ||
|
||
for (const ruleToSwitch of rules) { | ||
this.switchRuleState(ruleToSwitch, isEnabled, start, end); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since there's no
walk()
this could use a name change.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds reasonable. Open for suggestions