Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Walk Context #1

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/language/rule/abstractRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import * as ts from "typescript";

import {doesIntersect} from "../utils";
import {IWalker} from "../walker";
import {IWalker, WalkContext} from "../walker";
import {IDisabledInterval, IOptions, IRule, IRuleMetadata, RuleFailure} from "./rule";

export abstract class AbstractRule implements IRule {
Expand Down Expand Up @@ -57,6 +57,12 @@ export abstract class AbstractRule implements IRule {

public abstract apply(sourceFile: ts.SourceFile, languageService: ts.LanguageService): RuleFailure[];

public applyWithWalk(sourceFile: ts.SourceFile, walk: (ctx: WalkContext) => void): RuleFailure[] {
const ctx = new WalkContext(sourceFile, this.getOptions().ruleName);
walk(ctx);
return ctx.getFailures();
}

public applyWithWalker(walker: IWalker): RuleFailure[] {
walker.walk(walker.getSourceFile());
return this.filterFailures(walker.getFailures());
Expand Down
16 changes: 16 additions & 0 deletions src/language/rule/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,22 @@ export class Replacement {
return replacements.reduce((text, r) => r.apply(text), content);
}

public static replaceFromTo(start: number, end: number, text: string) {
return new Replacement(start, end - start, text);
}

public static deleteText(start: number, length: number) {
return new Replacement(start, length, "");
}

public static deleteFromTo(start: number, end: number) {
return new Replacement(start, end - start, "");
}

public static appendText(start: number, text: string) {
return new Replacement(start, 0, text);
}

constructor(private innerStart: number, private innerLength: number, private innerText: string) {
}

Expand Down
36 changes: 35 additions & 1 deletion src/language/walker/walker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,44 @@

import * as ts from "typescript";

import {RuleFailure} from "../rule/rule";
import {Fix, Replacement, RuleFailure} from "../rule/rule";

export interface IWalker {
getSourceFile(): ts.SourceFile;
walk(node: ts.Node): void;
getFailures(): RuleFailure[];
}

//move this somewhere else
export class WalkContext {
public readonly limit: number;
private readonly failures: RuleFailure[];

constructor(public readonly sourceFile: ts.SourceFile, public readonly ruleName: string) {
this.failures = [];
this.limit = sourceFile.getFullWidth();
}

public getFailures(): RuleFailure[] {
return this.failures;
}

public addFailureAt(start: number, width: number, failure: string, fix?: Fix) {
this.addFailure(start, start + width, failure, fix);
}

public addFailure(start: number, end: number, failure: string, fix?: Fix) {
this.failures.push(
new RuleFailure(this.sourceFile, Math.min(start, this.limit), Math.min(end, this.limit), failure, this.ruleName, fix),
);
}

/** Add a failure using a node's span. */
public addFailureAtNode(node: ts.Node, failure: string, fix?: Fix) {
this.addFailure(node.getStart(this.sourceFile), node.getEnd(), failure, fix);
}

public createFix(...replacements: Replacement[]) {
return new Fix(this.ruleName, replacements);
}
}
12 changes: 7 additions & 5 deletions src/rules/noNullKeywordRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,18 @@ export class Rule extends Lint.Rules.AbstractRule {
public static FAILURE_STRING = "Use 'undefined' instead of 'null'";

public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithWalker(new NullWalker(sourceFile, this.getOptions()));
return this.applyWithWalk(sourceFile, walk);
}
}

class NullWalker extends Lint.RuleWalker {
public visitNode(node: ts.Node) {
super.visitNode(node);
function walk(ctx: Lint.WalkContext): void {
ts.forEachChild(ctx.sourceFile, cb);

function cb(node: ts.Node) {
if (node.kind === ts.SyntaxKind.NullKeyword && !isPartOfType(node)) {
this.addFailureAtNode(node, Rule.FAILURE_STRING);
ctx.addFailureAtNode(node, Rule.FAILURE_STRING);
}
ts.forEachChild(node, cb);
}
}

Expand Down
170 changes: 73 additions & 97 deletions src/rules/semicolonRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,122 +67,98 @@ export class Rule extends Lint.Rules.AbstractRule {
public static FAILURE_STRING_UNNECESSARY = "Unnecessary semicolon";

public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithWalker(new SemicolonWalker(sourceFile, this.getOptions()));
return this.applyWithWalk(sourceFile, ctx => walk(ctx, parseOptions(this.getOptions().ruleArguments)));
}
}

class SemicolonWalker extends Lint.RuleWalker {
public visitVariableStatement(node: ts.VariableStatement) {
this.checkSemicolonAt(node);
super.visitVariableStatement(node);
}

public visitExpressionStatement(node: ts.ExpressionStatement) {
this.checkSemicolonAt(node);
super.visitExpressionStatement(node);
}

public visitReturnStatement(node: ts.ReturnStatement) {
this.checkSemicolonAt(node);
super.visitReturnStatement(node);
}

public visitBreakStatement(node: ts.BreakOrContinueStatement) {
this.checkSemicolonAt(node);
super.visitBreakStatement(node);
}

public visitContinueStatement(node: ts.BreakOrContinueStatement) {
this.checkSemicolonAt(node);
super.visitContinueStatement(node);
}

public visitThrowStatement(node: ts.ThrowStatement) {
this.checkSemicolonAt(node);
super.visitThrowStatement(node);
}

public visitImportDeclaration(node: ts.ImportDeclaration) {
this.checkSemicolonAt(node);
super.visitImportDeclaration(node);
}

public visitImportEqualsDeclaration(node: ts.ImportEqualsDeclaration) {
this.checkSemicolonAt(node);
super.visitImportEqualsDeclaration(node);
}

public visitDoStatement(node: ts.DoStatement) {
this.checkSemicolonAt(node);
super.visitDoStatement(node);
type Options = Record<"ignoreInterfaces" | "ignoreBoundClassMethods" | "never" | "always", boolean>;
function parseOptions(options: any[]): Options {
const never = hasOption(OPTION_NEVER);
return {
ignoreInterfaces: hasOption(OPTION_IGNORE_INTERFACES),
ignoreBoundClassMethods: hasOption(OPTION_IGNORE_BOUND_CLASS_METHODS),
never,
// Backwards compatible with plain {"semicolon": true}
always: !never && (hasOption(OPTION_ALWAYS) || (options && options.length === 0))
}

public visitDebuggerStatement(node: ts.Statement) {
this.checkSemicolonAt(node);
super.visitDebuggerStatement(node);
//TODO: parser utilities
function hasOption(s: string){
return options.indexOf(s) !== -1;
}
}

public visitPropertyDeclaration(node: ts.PropertyDeclaration) {
const initializer = node.initializer;

// check if this is a multi-line arrow function (`[^]` in the regex matches all characters including CR & LF)
if (initializer && initializer.kind === ts.SyntaxKind.ArrowFunction && /\{[^]*\n/.test(node.getText())) {
if (!this.hasOption(OPTION_IGNORE_BOUND_CLASS_METHODS)) {
this.checkSemicolonAt(node, "never");
function walk(ctx: Lint.WalkContext, options: Options) {
const { sourceFile } = ctx;
cb(sourceFile);
return;

function cb(node: ts.Node) {
switch (node.kind) {
case ts.SyntaxKind.VariableStatement:
case ts.SyntaxKind.ExpressionStatement:
case ts.SyntaxKind.ReturnStatement:
case ts.SyntaxKind.BreakStatement:
case ts.SyntaxKind.ContinueStatement:
case ts.SyntaxKind.ThrowStatement:
case ts.SyntaxKind.ImportDeclaration:
case ts.SyntaxKind.ImportEqualsDeclaration:
case ts.SyntaxKind.DoStatement:
case ts.SyntaxKind.DebuggerStatement:
case ts.SyntaxKind.ExportAssignment:
case ts.SyntaxKind.TypeAliasDeclaration:
checkSemicolonAt(node);
break;

case ts.SyntaxKind.FunctionDeclaration:
if (!(node as ts.FunctionDeclaration).body) {
checkSemicolonAt(node);
}
break;

case ts.SyntaxKind.PropertyDeclaration: {
const { initializer } = node as ts.PropertyDeclaration;
// check if this is a multi-line arrow function (`[^]` in the regex matches all characters including CR & LF)
if (initializer && initializer.kind === ts.SyntaxKind.ArrowFunction && /\{[^]*\n/.test(node.getText())) {
if (!options.ignoreBoundClassMethods) {
checkSemicolonAt(node, "never");
}
} else {
checkSemicolonAt(node);
}
break;
}
} else {
this.checkSemicolonAt(node);
}
super.visitPropertyDeclaration(node);
}

public visitInterfaceDeclaration(node: ts.InterfaceDeclaration) {
if (this.hasOption(OPTION_IGNORE_INTERFACES)) {
return;
}

for (const member of node.members) {
this.checkSemicolonAt(member);
}
super.visitInterfaceDeclaration(node);
}
case ts.SyntaxKind.InterfaceDeclaration:
if (options.ignoreInterfaces) {
break;
}

public visitExportAssignment(node: ts.ExportAssignment) {
this.checkSemicolonAt(node);
super.visitExportAssignment(node);
}

public visitFunctionDeclaration(node: ts.FunctionDeclaration) {
if (!node.body) {
this.checkSemicolonAt(node);
for (const member of (node as ts.InterfaceDeclaration).members) {
checkSemicolonAt(member);
}
break;
}
super.visitFunctionDeclaration(node);
}

public visitTypeAliasDeclaration(node: ts.TypeAliasDeclaration) {
this.checkSemicolonAt(node);
super.visitTypeAliasDeclaration(node);
ts.forEachChild(node, cb);
}

private checkSemicolonAt(node: ts.Node, override?: "never") {
const sourceFile = this.getSourceFile();
function checkSemicolonAt(node: ts.Node, override?: "never") {
const hasSemicolon = Lint.childOfKind(node, ts.SyntaxKind.SemicolonToken) !== undefined;
const position = node.getStart(sourceFile) + node.getWidth(sourceFile);
const never = override === "never" || this.hasOption(OPTION_NEVER);
// Backwards compatible with plain {"semicolon": true}
const always = !never && (this.hasOption(OPTION_ALWAYS) || (this.getOptions() && this.getOptions().length === 0));
const never = override === "never" || options.never;

if (always && !hasSemicolon) {
if (options.always && !hasSemicolon) {
const children = node.getChildren(sourceFile);
const lastChild = children[children.length - 1];
if (node.parent!.kind === ts.SyntaxKind.InterfaceDeclaration && lastChild.kind === ts.SyntaxKind.CommaToken) {
const failureStart = lastChild.getStart(sourceFile);
const fix = this.createFix(this.createReplacement(failureStart, lastChild.getWidth(sourceFile), ";"));
this.addFailureAt(failureStart, 0, Rule.FAILURE_STRING_COMMA, fix);
const fix = ctx.createFix(new Lint.Replacement(failureStart, lastChild.getWidth(sourceFile), ";"));
ctx.addFailureAt(failureStart, 0, Rule.FAILURE_STRING_COMMA, fix);
} else {
const failureStart = Math.min(position, this.getLimit());
const fix = this.createFix(this.appendText(failureStart, ";"));
this.addFailureAt(failureStart, 0, Rule.FAILURE_STRING_MISSING, fix);
const failureStart = Math.min(position, ctx.limit);
const fix = ctx.createFix(Lint.Replacement.appendText(failureStart, ";"));
ctx.addFailureAt(failureStart, 0, Rule.FAILURE_STRING_MISSING, fix);
}
} else if (never && hasSemicolon) {
const scanner = ts.createScanner(ts.ScriptTarget.ES5, false, ts.LanguageVariant.Standard, sourceFile.text);
Expand All @@ -195,9 +171,9 @@ class SemicolonWalker extends Lint.RuleWalker {

if (tokenKind !== ts.SyntaxKind.OpenParenToken && tokenKind !== ts.SyntaxKind.OpenBracketToken
&& tokenKind !== ts.SyntaxKind.PlusToken && tokenKind !== ts.SyntaxKind.MinusToken) {
const failureStart = Math.min(position - 1, this.getLimit());
const fix = this.createFix(this.deleteText(failureStart, 1));
this.addFailureAt(failureStart, 1, Rule.FAILURE_STRING_UNNECESSARY, fix);
const failureStart = Math.min(position - 1, ctx.limit);
const fix = ctx.createFix(Lint.Replacement.deleteText(failureStart, 1));
ctx.addFailureAt(failureStart, 1, Rule.FAILURE_STRING_UNNECESSARY, fix);
}
}
}
Expand Down