Designing rule walkers
Using WalkContext and applyWithFunction
If you have a rule with a pretty simple implementation, you don’t need to declare a class which extends the Walker
class. Instead, you can define a callback function that accepts following argument:
ctx: WalkContext<T>
: An object containing rule information, an objectoptions: T
containing the parsed rule arguments, thets.sourceFile
object, and functions for adding failures
Use this callback as an argument to applyWithFunction
. You can also pass your parsed rule arguments as optional 3rd parameter.
Let’s look at no-null-keyword
as an example:
import * as ts from "typescript";
import * as Lint from "tslint";
export class Rule extends Lint.Rules.AbstractRule {
public static FAILURE_STRING = "Use 'undefined' instead of 'null'";
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
// Call `applyWithFunction` with your callback function, `walk`.
// This creates a `WalkContext<T>` and passes it in as an argument.
// An optional 3rd parameter allows you to pass in a parsed version of `this.ruleArguments`. If used, it is not recommended to
// simply pass in `this.getOptions()`, but to parse it into a more useful object instead.
return this.applyWithFunction(sourceFile, walk);
}
}
// Here, the options object type is `void` (which is the default, so can be omitted) because we don't pass any options in this example.
function walk(ctx: Lint.WalkContext<void>) {
// Recursively walk the AST starting with root node, `ctx.sourceFile`.
// Call the function `cb` (defined below) for each child.
return ts.forEachChild(ctx.sourceFile, cb);
function cb(node: ts.Node): void {
// Stop recursing further into the AST by returning early. Here, we ignore type nodes.
if (node.kind >= ts.SyntaxKind.FirstTypeNode && node.kind <= ts.SyntaxKind.LastTypeNode) {
return;
}
// Add failures using the `WalkContext<T>` object. Here, we add a failure if we find the null keyword.
if (node.kind === ts.SyntaxKind.NullKeyword) {
return ctx.addFailureAtNode(node, Rule.FAILURE_STRING);
}
// Continue recursion into the AST by calling function `cb` for every child of the current node.
return ts.forEachChild(node, cb);
}
}
Using AbstractWalker
If your rule implementation is a bit more involved than the above example, you can also implement it as a class.
Simply extend AbstractWalker<T>
and implement the walk
method.
import * as ts from "typescript";
import * as Lint from "tslint";
export class Rule extends Lint.Rules.AbstractRule {
public static FAILURE_STRING = "'magic numbers' are not allowed";
public static ALLOWED_NODES = new Set<ts.SyntaxKind>([
...
]);
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
// We convert the `ruleArguments` into a useful format before passing it to the constructor of AbstractWalker.
return this.applyWithWalker(new NoMagicNumbersWalker(sourceFile, this.ruleName, new Set(this.ruleArguments.map(String))));
}
}
// The type parameter of AbstractWalker corresponds to the third constructor parameter.
class NoMagicNumbersWalker extends Lint.AbstractWalker<Set<string>> {
public walk(sourceFile: ts.SourceFile) {
const cb = (node: ts.Node): void => {
// Finds specific node types and do checking.
if (node.kind === ts.SyntaxKind.NumericLiteral) {
this.checkNumericLiteral(node, (node as ts.NumericLiteral).text);
} else if (node.kind === ts.SyntaxKind.PrefixUnaryExpression &&
(node as ts.PrefixUnaryExpression).operator === ts.SyntaxKind.MinusToken) {
this.checkNumericLiteral(node, "-" + ((node as ts.PrefixUnaryExpression).operand as ts.NumericLiteral).text);
} else {
// Continue rescursion: call function `cb` for all children of the current node.
return ts.forEachChild(node, cb);
}
};
// Start recursion for all children of `sourceFile`.
return ts.forEachChild(sourceFile, cb);
}
private checkNumericLiteral(node: ts.Node, num: string) {
// `this.options` is the third constructor parameter from above (the Set we created in `Rule.apply`)
if (!Rule.ALLOWED_NODES.has(node.parent!.kind) && !this.options.has(num)) {
// Add failures to the Walker.
this.addFailureAtNode(node, Rule.FAILURE_STRING);
}
}
}
Migrating from RuleWalker to AbstractWalker
The main difference between RuleWalker
and AbstractWalker
is that you need to implement the AST recursion yourself. But why would you want to do that?
Performance! RuleWalker
wants to be “one walker to rule them all” (pun intended). It’s easy to use but that convenience
makes it slow by default. When implementing the walking yourself, you only need to do as much work as needed.
Besides that you should convert the ruleArguments
to a useful format before passing it to AbstractWalker
as seen above.
This table describes the equivalent methods between the two classes:
RuleWalker |
AbstractWalker |
---|---|
this.createFailure() and this.addFailure() |
this.addFailureAt() |
this.addFailureFromStartToEnd() |
this.addFailure() |
this.createReplacement() |
new Lint.Replacement() |
this.deleteText() |
Lint.Replacement.deleteText() |
this.deleteFromTo() |
Lint.Replacement.deleteFromTo() |
this.appendText() |
Lint.Replacement.appendText() |
this.hasOption() and this.getOptions() |
use this.options directly |
this.getLineAndCharacterOfPosition() |
ts.getLineAndCharacterOfPosition(this.sourceFile, ...) |
this.getLimit() |
this.sourceFile.end |
this.getSourceFile() |
is available to be compatible, but prefer this.sourceFile |
this.getFailures() |
is available to be compatible, but prefer this.failures |
this.skip() |
just don’t use it, it’s a noop |
this.getRuleName() |
this.ruleName |