blob: 024c0087947b1d1575d84198a858696ba49f7347 [file] [log] [blame]
/**
* @fileoverview This is a collection of smaller utility functions to operate on
* a TypeScript AST, used by JSConformance rules and elsewhere.
*/
import * as ts from 'typescript';
/**
* Triggers increased verbosity in the rules.
*/
let DEBUG = false;
/**
* Turns on or off logging for ConformancePatternRules.
*/
export function setDebug(state: boolean) {
DEBUG = state;
}
/**
* Debug helper.
*/
export function debugLog(msg: string) {
if (DEBUG) console.log(msg);
}
/**
* Returns `n`'s parents in order.
*/
export function parents(n: ts.Node): ts.Node[] {
const p = [];
while (n.parent) {
n = n.parent;
p.push(n);
}
return p;
}
/**
* Searches for something satisfying the given test in `n` or its children.
*/
export function findInChildren(
n: ts.Node, test: (n: ts.Node) => boolean): boolean {
let toExplore: ts.Node[] = [n];
let cur: ts.Node|undefined;
while (cur = toExplore.pop()) {
if (test(cur)) {
return true;
}
// Recurse
toExplore = toExplore.concat(cur.getChildren());
}
return false;
}
/**
* Returns true if the pattern-based Rule should look at that node and consider
* warning there. The goal is to make it easy to exclude on source files,
* blocks, module declarations, JSDoc, lib.d.ts nodes, that kind of things.
*/
export function shouldExamineNode(n: ts.Node) {
return !(
ts.isBlock(n) || ts.isModuleBlock(n) || ts.isModuleDeclaration(n) ||
ts.isSourceFile(n) || (n.parent && ts.isTypeNode(n.parent)) ||
ts.isJSDoc(n) || isInStockLibraries(n));
}
/**
* Return whether the given declaration is ambient.
*/
export function isAmbientDeclaration(d: ts.Declaration): boolean {
return Boolean(
d.modifiers &&
d.modifiers.some(m => m.kind === ts.SyntaxKind.DeclareKeyword));
}
/**
* Return whether the given Node is (or is in) a library included as default.
* We currently look for a node_modules/typescript/ prefix, but this could
* be expanded if needed.
*/
export function isInStockLibraries(n: ts.Node|ts.SourceFile): boolean {
const sourceFile = ts.isSourceFile(n) ? n : n.getSourceFile();
if (sourceFile) {
return sourceFile.fileName.indexOf('node_modules/typescript/') !== -1;
} else {
// the node is nowhere? Consider it as part of the core libs: we can't do
// anything with it anyways, and it was likely included as default.
return true;
}
}
/**
* Turns the given Symbol into its non-aliased version (which could be itself).
* Returns undefined if given an undefined Symbol (so you can call
* `dealias(typeChecker.getSymbolAtLocation(node))`).
*/
export function dealias(
symbol: ts.Symbol|undefined, tc: ts.TypeChecker): ts.Symbol|undefined {
if (!symbol) {
return undefined;
}
if (symbol.getFlags() & ts.SymbolFlags.Alias) {
// Note: something that has only TypeAlias is not acceptable here.
return dealias(tc.getAliasedSymbol(symbol), tc);
}
return symbol;
}
/**
* Returns whether `n`'s parents are something indicating a type.
*/
export function isPartOfTypeDeclaration(n: ts.Node) {
return [n, ...parents(n)].some(
p => p.kind === ts.SyntaxKind.TypeReference ||
p.kind === ts.SyntaxKind.TypeLiteral);
}
/**
* Returns whether `n` is under an import statement.
*/
export function isPartOfImportStatement(n: ts.Node) {
return [n, ...parents(n)].some(
p => p.kind === ts.SyntaxKind.ImportDeclaration);
}
/**
* Returns whether `n` is a declaration.
*/
export function isDeclaration(n: ts.Node): n is ts.VariableDeclaration|
ts.ClassDeclaration|ts.FunctionDeclaration|ts.MethodDeclaration|
ts.PropertyDeclaration|ts.VariableDeclarationList|ts.InterfaceDeclaration|
ts.TypeAliasDeclaration|ts.EnumDeclaration|ts.ModuleDeclaration|
ts.ImportDeclaration|ts.ImportEqualsDeclaration|ts.ExportDeclaration|
ts.MissingDeclaration {
return ts.isVariableDeclaration(n) || ts.isClassDeclaration(n) ||
ts.isFunctionDeclaration(n) || ts.isMethodDeclaration(n) ||
ts.isPropertyDeclaration(n) || ts.isVariableDeclarationList(n) ||
ts.isInterfaceDeclaration(n) || ts.isTypeAliasDeclaration(n) ||
ts.isEnumDeclaration(n) || ts.isModuleDeclaration(n) ||
ts.isImportDeclaration(n) || ts.isImportEqualsDeclaration(n) ||
ts.isExportDeclaration(n) || ts.isMissingDeclaration(n);
}
/** Type guard for expressions that looks like property writes. */
export function isPropertyWriteExpression(node: ts.Node):
node is(ts.BinaryExpression & {
left: ts.PropertyAccessExpression;
}) {
if (!ts.isBinaryExpression(node)) {
return false;
}
if (node.operatorToken.getText().trim() !== '=') {
return false;
}
if (!ts.isPropertyAccessExpression(node.left) ||
node.left.expression.getFullText().trim() === '') {
return false;
}
// TODO: Destructuring assigments aren't covered. This would be a potential
// bypass, but I doubt we'd catch bugs, so fixing it seems low priority
// overall.
return true;
}
/**
* If verbose, logs the given error that happened while walking n, with a
* stacktrace.
*/
export function logASTWalkError(verbose: boolean, n: ts.Node, e: Error) {
let nodeText = `[error getting name for ${JSON.stringify(n)}]`;
try {
nodeText = '"' + n.getFullText().trim() + '"';
} catch {
}
debugLog(
`Walking node ${nodeText} failed with error ${e}.\n` +
`Stacktrace:\n${e.stack}`);
}