blob: fcad3c71b5b338e2ccc63a53d69017c5c9789928 [file]
/**
* @fileoverview Checker contains all the information we need to perform source
* file AST traversals and report errors.
*/
import * as ts from 'typescript';
import {Failure, Fix} from './failure';
/**
* A Handler contains a handler function and its corresponding error code so
* when the handler function is triggered we know which rule is violated.
*/
interface Handler<T extends ts.Node> {
handlerFunction(checker: Checker, node: T): void;
code: number;
}
/**
* Tsetse rules use on() and addFailureAtNode() for rule implementations.
* Rules can get a ts.TypeChecker from checker.typeChecker so typed rules are
* possible. Compiler uses execute() to run the Tsetse check.
*/
export class Checker {
/** Node to handlers mapping for all enabled rules. */
private readonly nodeHandlersMap =
new Map<ts.SyntaxKind, Array<Handler<ts.Node>>>();
/**
* Mapping from identifier name to handlers for all rules inspecting property
* names.
*/
private readonly namedIdentifierHandlersMap =
new Map<string, Array<Handler<ts.Identifier>>>();
/**
* Mapping from property name to handlers for all rules inspecting property
* accesses expressions.
*/
private readonly namedPropertyAccessHandlersMap =
new Map<string, Array<Handler<ts.PropertyAccessExpression>>>();
/**
* Mapping from string literal value to handlers for all rules inspecting
* string literals.
*/
private readonly stringLiteralElementAccessHandlersMap =
new Map<string, Array<Handler<ts.ElementAccessExpression>>>();
private failures: Failure[] = [];
private currentSourceFile: ts.SourceFile|undefined;
// currentCode will be set before invoking any handler functions so the value
// initialized here is never used.
private currentCode = 0;
/** Allow typed rules via typeChecker. */
typeChecker: ts.TypeChecker;
constructor(program: ts.Program) {
// Avoid the cost for each rule to create a new TypeChecker.
this.typeChecker = program.getTypeChecker();
}
/**
* This doesn't run any checks yet. Instead, it registers `handlerFunction` on
* `nodeKind` node in `nodeHandlersMap` map. After all rules register their
* handlers, the source file AST will be traversed.
*/
on<T extends ts.Node>(
nodeKind: T['kind'], handlerFunction: (checker: Checker, node: T) => void,
code: number) {
const newHandler: Handler<T> = {handlerFunction, code};
const registeredHandlers = this.nodeHandlersMap.get(nodeKind);
if (registeredHandlers === undefined) {
this.nodeHandlersMap.set(nodeKind, [newHandler]);
} else {
registeredHandlers.push(newHandler);
}
}
/**
* Similar to `on`, but registers handlers on more specific node type, i.e.,
* identifiers.
*/
onNamedIdentifier(
identifierName: string,
handlerFunction: (checker: Checker, node: ts.Identifier) => void,
code: number) {
const newHandler: Handler<ts.Identifier> = {handlerFunction, code};
const registeredHandlers =
this.namedIdentifierHandlersMap.get(identifierName);
if (registeredHandlers === undefined) {
this.namedIdentifierHandlersMap.set(identifierName, [newHandler]);
} else {
registeredHandlers.push(newHandler);
}
}
/**
* Similar to `on`, but registers handlers on more specific node type, i.e.,
* property access expressions.
*/
onNamedPropertyAccess(
propertyName: string,
handlerFunction:
(checker: Checker, node: ts.PropertyAccessExpression) => void,
code: number) {
const newHandler:
Handler<ts.PropertyAccessExpression> = {handlerFunction, code};
const registeredHandlers =
this.namedPropertyAccessHandlersMap.get(propertyName);
if (registeredHandlers === undefined) {
this.namedPropertyAccessHandlersMap.set(propertyName, [newHandler]);
} else {
registeredHandlers.push(newHandler);
}
}
/**
* Similar to `on`, but registers handlers on more specific node type, i.e.,
* element access expressions with string literals as keys.
*/
onStringLiteralElementAccess(
key: string,
handlerFunction:
(checker: Checker, node: ts.ElementAccessExpression) => void,
code: number) {
const newHandler:
Handler<ts.ElementAccessExpression> = {handlerFunction, code};
const registeredHandlers =
this.stringLiteralElementAccessHandlersMap.get(key);
if (registeredHandlers === undefined) {
this.stringLiteralElementAccessHandlersMap.set(key, [newHandler]);
} else {
registeredHandlers.push(newHandler);
}
}
/**
* Add a failure with a span.
*/
addFailure(start: number, end: number, failureText: string, fix?: Fix) {
if (!this.currentSourceFile) {
throw new Error('Source file not defined');
}
if (start >= end || end > this.currentSourceFile.end || start < 0) {
// Since only addFailureAtNode() is exposed for now this shouldn't happen.
throw new Error(
`Invalid start and end position: [${start}, ${end}]` +
` in file ${this.currentSourceFile.fileName}.`);
}
const failure = new Failure(
this.currentSourceFile, start, end, failureText, this.currentCode, fix);
this.failures.push(failure);
}
addFailureAtNode(node: ts.Node, failureText: string, fix?: Fix) {
// node.getStart() takes a sourceFile as argument whereas node.getEnd()
// doesn't need it.
this.addFailure(
node.getStart(this.currentSourceFile), node.getEnd(), failureText, fix);
}
/** Dispatch general handlers registered via `on` */
dispatchNodeHandlers(node: ts.Node) {
const handlers = this.nodeHandlersMap.get(node.kind);
if (handlers === undefined) return;
for (const handler of handlers) {
this.currentCode = handler.code;
handler.handlerFunction(this, node);
}
}
/** Dispatch identifier handlers registered via `onNamedIdentifier` */
dispatchNamedIdentifierHandlers(id: ts.Identifier) {
const handlers = this.namedIdentifierHandlersMap.get(id.text);
if (handlers === undefined) return;
for (const handler of handlers) {
this.currentCode = handler.code;
handler.handlerFunction(this, id);
}
}
/**
* Dispatch property access handlers registered via `onNamedPropertyAccess`
*/
dispatchNamedPropertyAccessHandlers(prop: ts.PropertyAccessExpression) {
const handlers = this.namedPropertyAccessHandlersMap.get(prop.name.text);
if (handlers === undefined) return;
for (const handler of handlers) {
this.currentCode = handler.code;
handler.handlerFunction(this, prop);
}
}
/**
* Dispatch string literal handlers registered via
* `onStringLiteralElementAccess`.
*/
dispatchStringLiteralElementAccessHandlers(elem: ts.ElementAccessExpression) {
const ty = this.typeChecker.getTypeAtLocation(elem.argumentExpression);
if (!ty.isStringLiteral()) return;
const handlers = this.stringLiteralElementAccessHandlersMap.get(ty.value);
if (handlers === undefined) return;
for (const handler of handlers) {
this.currentCode = handler.code;
handler.handlerFunction(this, elem);
}
}
/**
* Walk `sourceFile`, invoking registered handlers with Checker as the first
* argument and current node as the second argument. Return failures if there
* are any.
*/
execute(sourceFile: ts.SourceFile): Failure[] {
const thisChecker = this;
this.currentSourceFile = sourceFile;
this.failures = [];
run(sourceFile);
return this.failures;
function run(node: ts.Node) {
// Dispatch handlers registered via `on`
thisChecker.dispatchNodeHandlers(node);
// Dispatch handlers for named identifiers and properties
if (ts.isIdentifier(node)) {
thisChecker.dispatchNamedIdentifierHandlers(node);
} else if (ts.isPropertyAccessExpression(node)) {
thisChecker.dispatchNamedPropertyAccessHandlers(node);
} else if (ts.isElementAccessExpression(node)) {
thisChecker.dispatchStringLiteralElementAccessHandlers(node);
}
ts.forEachChild(node, run);
}
}
}