plaxtony
Version:
Static code analysis of SC2 Galaxy Script
519 lines (467 loc) • 21.7 kB
text/typescript
import * as lsp from 'vscode-languageserver';
import * as gt from './types';
/**
* True if node is of some token syntax kind.
* For example, this is true for an IfKeyword but not for an IfStatement.
*/
export function isToken(n: gt.Node): boolean {
return <number>n.kind >= gt.SyntaxKindMarker.FirstToken && <number>n.kind <= gt.SyntaxKindMarker.LastToken;
}
export function isModifierKind(token: gt.SyntaxKind): boolean {
switch (token) {
case gt.SyntaxKind.ConstKeyword:
case gt.SyntaxKind.StaticKeyword:
case gt.SyntaxKind.NativeKeyword:
return true;
}
return false;
}
export function isKeywordKind(token: gt.SyntaxKind): boolean {
return <number>token >= gt.SyntaxKindMarker.FirstKeyword && <number>token <= gt.SyntaxKindMarker.LastKeyword;
}
export function isKeywordTypeKind(token: gt.SyntaxKind): boolean {
return <number>token >= gt.SyntaxKindMarker.FirstKeywordType && <number>token <= gt.SyntaxKindMarker.LastKeywordType;
}
export function isComplexTypeKind(token: gt.SyntaxKind): boolean {
if (gt.SyntaxKindMarker.FirstComplexType <= <number>token && gt.SyntaxKindMarker.LastComplexType >= <number>token) {
return true;
}
return false;
}
export function isReferenceKeywordKind(token: gt.SyntaxKind): boolean {
switch (token) {
case gt.SyntaxKind.ArrayrefKeyword:
case gt.SyntaxKind.StructrefKeyword:
case gt.SyntaxKind.FuncrefKeyword:
return true;
}
return false;
}
export function isComparisonOperator(token: gt.SyntaxKind): boolean {
return token >= gt.SyntaxKind.LessThanToken && token <= gt.SyntaxKind.EqualsGreaterThanToken;
}
export function isAssignmentOperator(token: gt.SyntaxKind): boolean {
return token >= gt.SyntaxKind.EqualsToken && token <= gt.SyntaxKind.CaretEqualsToken;
}
export function isAssignmentExpression(node: gt.Node): boolean {
switch (node.kind) {
case gt.SyntaxKind.BinaryExpression:
return isAssignmentOperator((<gt.BinaryExpression>node).operatorToken.kind);
case gt.SyntaxKind.ParenthesizedExpression:
return isAssignmentExpression((<gt.ParenthesizedExpression>node).expression);
default:
return false;
}
}
export function isLeftHandSideExpressionKind(kind: gt.SyntaxKind): boolean {
return kind === gt.SyntaxKind.PropertyAccessExpression
|| kind === gt.SyntaxKind.ElementAccessExpression
|| kind === gt.SyntaxKind.CallExpression
|| kind === gt.SyntaxKind.ParenthesizedExpression
|| kind === gt.SyntaxKind.ArrayLiteralExpression
|| kind === gt.SyntaxKind.Identifier
|| kind === gt.SyntaxKind.NumericLiteral
|| kind === gt.SyntaxKind.StringLiteral
|| kind === gt.SyntaxKind.FalseKeyword
|| kind === gt.SyntaxKind.NullKeyword
|| kind === gt.SyntaxKind.TrueKeyword;
}
export function isContainerKind(kind: gt.SyntaxKind): boolean {
return kind === gt.SyntaxKind.SourceFile
|| kind === gt.SyntaxKind.FunctionDeclaration
|| kind === gt.SyntaxKind.StructDeclaration
;
}
export function isNamedDeclarationKind(kind: gt.SyntaxKind): boolean {
return kind === gt.SyntaxKind.SourceFile
|| kind === gt.SyntaxKind.VariableDeclaration
|| kind === gt.SyntaxKind.FunctionDeclaration
|| kind === gt.SyntaxKind.StructDeclaration
|| kind === gt.SyntaxKind.PropertyDeclaration
|| kind === gt.SyntaxKind.ParameterDeclaration
|| kind === gt.SyntaxKind.TypedefDeclaration
;
}
export function isDeclarationKind(kind: gt.SyntaxKind): boolean {
return isNamedDeclarationKind(kind)
;
}
export function isLeftHandSideExpression(node: gt.Node): boolean {
return isLeftHandSideExpressionKind(node.kind);
}
export function isPartOfExpression(node: gt.Node): boolean {
switch (node.kind) {
case gt.SyntaxKind.NullKeyword:
case gt.SyntaxKind.TrueKeyword:
case gt.SyntaxKind.FalseKeyword:
case gt.SyntaxKind.ArrayLiteralExpression:
case gt.SyntaxKind.PropertyAccessExpression:
case gt.SyntaxKind.ElementAccessExpression:
case gt.SyntaxKind.CallExpression:
case gt.SyntaxKind.TypeAssertionExpression:
case gt.SyntaxKind.ParenthesizedExpression:
case gt.SyntaxKind.PrefixUnaryExpression:
case gt.SyntaxKind.PostfixUnaryExpression:
case gt.SyntaxKind.BinaryExpression:
case gt.SyntaxKind.Identifier:
return true;
case gt.SyntaxKind.NumericLiteral:
case gt.SyntaxKind.StringLiteral:
const parent = node.parent;
switch (parent.kind) {
case gt.SyntaxKind.VariableDeclaration:
case gt.SyntaxKind.PropertyDeclaration:
case gt.SyntaxKind.ExpressionStatement:
case gt.SyntaxKind.IfStatement:
case gt.SyntaxKind.DoStatement:
case gt.SyntaxKind.WhileStatement:
case gt.SyntaxKind.ReturnStatement:
case gt.SyntaxKind.ForStatement:
const forStatement = <gt.ForStatement>parent;
return (forStatement.initializer === node) ||
forStatement.condition === node ||
forStatement.incrementor === node;
default:
if (isPartOfExpression(parent)) {
return true;
}
}
}
return false;
}
export function isPartOfTypeNode(node: gt.Node): boolean {
if (gt.SyntaxKindMarker.FirstTypeNode <= <number>node.kind && <number>node.kind <= gt.SyntaxKindMarker.LastTypeNode) {
return true;
}
switch (node.kind) {
case gt.SyntaxKind.IntKeyword:
case gt.SyntaxKind.FixedKeyword:
case gt.SyntaxKind.StringKeyword:
case gt.SyntaxKind.BoolKeyword:
case gt.SyntaxKind.VoidKeyword:
return true;
// Identifiers and qualified names may be type nodes, depending on their context. Climb
// above them to find the lowest container
case gt.SyntaxKind.Identifier:
// If the identifier is the RHS of a qualified name, then it's a type iff its parent is.
if (node.parent.kind === gt.SyntaxKind.PropertyAccessExpression && (<gt.PropertyAccessExpression>node.parent).name === node) {
node = node.parent;
}
// At this point, node is either a qualified name or an identifier
// Debug.assert(node.kind === gt.SyntaxKind.Identifier || node.kind === gt.SyntaxKind.QualifiedName || node.kind === gt.SyntaxKind.PropertyAccessExpression,
// "'node' was expected to be a qualified name, identifier or property access in 'isPartOfTypeNode'.");
// falls through
case gt.SyntaxKind.PropertyAccessExpression:
const parent = node.parent;
// Do not recursively call isPartOfTypeNode on the parent. In the example:
//
// let a: A.B.C;
//
// Calling isPartOfTypeNode would consider the qualified name A.B a type node.
// Only C and A.B.C are type nodes.
if (gt.SyntaxKindMarker.FirstTypeNode <= <number>parent.kind && <number>parent.kind <= gt.SyntaxKindMarker.LastTypeNode) {
return true;
}
switch (parent.kind) {
case gt.SyntaxKind.PropertyDeclaration:
case gt.SyntaxKind.ParameterDeclaration:
case gt.SyntaxKind.VariableDeclaration:
return node === (<gt.VariableDeclaration>parent).type;
case gt.SyntaxKind.FunctionDeclaration:
return node === (<gt.FunctionDeclaration>parent).type;
// TODO:
// case gt.SyntaxKind.CallExpression:
// return (<gt.CallExpression>parent).typeArguments && indexOf((<gt.CallExpression>parent).typeArguments, node) >= 0;
}
}
return false;
}
export function isRightSideOfPropertyAccess(node: gt.Node) {
return (node.parent.kind === gt.SyntaxKind.PropertyAccessExpression && (<gt.PropertyAccessExpression>node.parent).name === node);
}
function isNodeOrArray(a: any): boolean {
return a !== undefined && a.kind !== undefined;
}
export function getKindName(k: number | string): string {
if (typeof k === "string") {
return k;
}
return (<any>gt).SyntaxKind[k];
}
export function sourceFileToJSON(file: gt.Node): string {
return JSON.stringify(file, (_, v) => isNodeOrArray(v) ? serializeNode(v) : v, " ");
function serializeNode(n: gt.Node): any {
const o: any = { kind: getKindName(n.kind) };
// if (ts.containsParseError(n)) {
// o.containsParseError = true;
// }
for (let propertyName in n) {
switch (propertyName) {
case "parent":
case "symbol":
case "locals":
case "localSymbol":
case "kind":
case "semanticDiagnostics":
case "id":
case "nodeCount":
case "symbolCount":
case "identifierCount":
case "scriptSnapshot":
// Blacklist of items we never put in the baseline file.
break;
case "originalKeywordKind":
o[propertyName] = getKindName((<any>n)[propertyName]);
break;
case "flags":
// Clear the flags that are produced by aggregating child values. That is ephemeral
// data we don't care about in the dump. We only care what the parser set directly
// on the AST.
// const flags = n.flags & ~(ts.NodeFlags.JavaScriptFile | ts.NodeFlags.HasAggregatedChildData);
// if (flags) {
// o[propertyName] = getNodeFlagName(flags);
// }
break;
case "referenceDiagnostics":
case "parseDiagnostics":
// o[propertyName] = Utils.convertDiagnostics((<any>n)[propertyName]);
break;
// case "nextContainer":
// if (n.nextContainer) {
// o[propertyName] = { kind: n.nextContainer.kind, pos: n.nextContainer.pos, end: n.nextContainer.end };
// }
// break;
case "text":
// Include 'text' field for identifiers/literals, but not for source files.
if (n.kind !== gt.SyntaxKind.SourceFile) {
o[propertyName] = (<any>n)[propertyName];
}
break;
default:
o[propertyName] = (<any>n)[propertyName];
}
}
return o;
}
}
/**
* Iterates through the parent chain of a node and performs the callback on each parent until the callback
* returns a truthy value, then returns that value.
* If no such value is found, it applies the callback until the parent pointer is undefined or the callback returns "quit"
* At that point findAncestor returns undefined.
*/
export function findAncestor<T extends gt.Node>(node: gt.Node, callback: (element: gt.Node) => element is T): T | undefined;
export function findAncestor(node: gt.Node, callback: (element: gt.Node) => boolean | "quit"): gt.Node | undefined;
export function findAncestor(node: gt.Node, callback: (element: gt.Node) => boolean | "quit"): gt.Node {
while (node) {
const result = callback(node);
if (result === "quit") {
return undefined;
}
else if (result) {
return node;
}
node = node.parent;
}
return undefined;
}
export function findAncestorByKind(node: gt.Node, kind: gt.SyntaxKind): gt.Node {
while (node && node.kind !== kind) {
node = node.parent;
}
return node;
}
export function getSourceFileOfNode(node: gt.Node): gt.SourceFile {
while (node && node.kind !== gt.SyntaxKind.SourceFile) {
node = node.parent;
}
return <gt.SourceFile>node;
}
export function fixupParentReferences(rootNode: gt.Node) {
let parent: gt.Node = rootNode;
forEachChild(rootNode, visitNode);
function visitNode(n: gt.Node): void {
// walk down setting parents that differ from the parent we think it should be. This
// allows us to quickly bail out of setting parents for subtrees during incremental
// parsing
if (n.parent !== parent) {
n.parent = parent;
const saveParent = parent;
parent = n;
forEachChild(n, visitNode);
parent = saveParent;
}
}
}
function visitNode<T>(cbNode: (node?: gt.Node) => T, node: gt.Node): T | undefined {
return node && cbNode(node);
}
function visitNodes<T>(cbNode: (node: gt.Node) => T, cbNodes: (node: gt.NodeArray<gt.Node>) => T | undefined, nodes: gt.NodeArray<gt.Node>): T | undefined {
if (nodes) {
if (cbNodes) {
return cbNodes(nodes);
}
for (const node of nodes) {
const result = cbNode(node);
if (result) {
return result;
}
}
}
}
/**
* Invokes a callback for each child of the given node. The 'cbNode' callback is invoked for all child nodes
* stored in properties. If a 'cbNodes' callback is specified, it is invoked for embedded arrays; otherwise,
* embedded arrays are flattened and the 'cbNode' callback is invoked for each element. If a callback returns
* a truthy value, iteration stops and that value is returned. Otherwise, undefined is returned.
*
* @param node a given node to visit its children
* @param cbNode a callback to be invoked for all child nodes
* @param cbNodes a callback to be invoked for embedded array
*
* @remarks `forEachChild` must visit the children of a node in the order
* that they appear in the source code.
*/
export function forEachChild<T>(node: gt.Node, cbNode: (node: gt.Node) => T | undefined, cbNodes?: (nodes: gt.NodeArray<gt.Node>) => T | undefined): T | undefined {
if (!node || !node.kind) {
return;
}
switch (node.kind) {
case gt.SyntaxKind.PropertyDeclaration:
return visitNodes(cbNode, cbNodes, (<gt.PropertyDeclaration>node).modifiers) ||
visitNode(cbNode, (<gt.PropertyDeclaration>node).type) ||
visitNode(cbNode, (<gt.PropertyDeclaration>node).name);
case gt.SyntaxKind.VariableDeclaration:
return visitNodes(cbNode, cbNodes, (<gt.VariableDeclaration>node).modifiers) ||
visitNode(cbNode, (<gt.VariableDeclaration>node).type) ||
visitNode(cbNode, (<gt.VariableDeclaration>node).name) ||
visitNode(cbNode, (<gt.VariableDeclaration>node).initializer);
case gt.SyntaxKind.FunctionDeclaration:
return visitNodes(cbNode, cbNodes, (<gt.FunctionDeclaration>node).modifiers) ||
visitNode(cbNode, (<gt.FunctionDeclaration>node).type) ||
visitNode(cbNode, (<gt.FunctionDeclaration>node).name) ||
visitNodes(cbNode, cbNodes, (<gt.FunctionDeclaration>node).parameters) ||
visitNode(cbNode, (<gt.FunctionDeclaration>node).body);
case gt.SyntaxKind.StructDeclaration:
return visitNodes(cbNode, cbNodes, (<gt.StructDeclaration>node).modifiers) ||
visitNode(cbNode, (<gt.StructDeclaration>node).name) ||
visitNodes(cbNode, cbNodes, (<gt.StructDeclaration>node).members);
case gt.SyntaxKind.ParameterDeclaration:
return visitNodes(cbNode, cbNodes, (<gt.ParameterDeclaration>node).modifiers) ||
visitNode(cbNode, (<gt.ParameterDeclaration>node).name) ||
visitNode(cbNode, (<gt.ParameterDeclaration>node).type);
case gt.SyntaxKind.TypedefDeclaration:
return visitNode(cbNode, (<gt.TypedefDeclaration>node).type) ||
visitNode(cbNode, (<gt.TypedefDeclaration>node).name);
case gt.SyntaxKind.ArrayType:
return visitNode(cbNode, (<gt.ArrayTypeNode>node).elementType) ||
visitNode(cbNode, (<gt.ArrayTypeNode>node).size);
case gt.SyntaxKind.MappedType:
return visitNode(cbNode, (<gt.MappedTypeNode>node).returnType) ||
visitNodes(cbNode, cbNodes, (<gt.MappedTypeNode>node).typeArguments);
case gt.SyntaxKind.PropertyAccessExpression:
return visitNode(cbNode, (<gt.PropertyAccessExpression>node).expression) ||
visitNode(cbNode, (<gt.PropertyAccessExpression>node).name);
case gt.SyntaxKind.ElementAccessExpression:
return visitNode(cbNode, (<gt.ElementAccessExpression>node).expression) ||
visitNode(cbNode, (<gt.ElementAccessExpression>node).argumentExpression);
case gt.SyntaxKind.CallExpression:
return visitNode(cbNode, (<gt.CallExpression>node).expression) ||
visitNodes(cbNode, cbNodes, (<gt.CallExpression>node).arguments);
case gt.SyntaxKind.ParenthesizedExpression:
return visitNode(cbNode, (<gt.ParenthesizedExpression>node).expression);
case gt.SyntaxKind.PrefixUnaryExpression:
return visitNode(cbNode, (<gt.PrefixUnaryExpression>node).operator) ||
visitNode(cbNode, (<gt.PrefixUnaryExpression>node).operand);
case gt.SyntaxKind.PostfixUnaryExpression:
return visitNode(cbNode, (<gt.PostfixUnaryExpression>node).operand) ||
visitNode(cbNode, (<gt.PostfixUnaryExpression>node).operator);
case gt.SyntaxKind.BinaryExpression:
return visitNode(cbNode, (<gt.BinaryExpression>node).left) ||
visitNode(cbNode, (<gt.BinaryExpression>node).operatorToken) ||
visitNode(cbNode, (<gt.BinaryExpression>node).right);
case gt.SyntaxKind.Block:
return visitNodes(cbNode, cbNodes, (<gt.Block>node).statements);
case gt.SyntaxKind.SourceFile:
return visitNodes(cbNode, cbNodes, (<gt.SourceFile>node).statements);
case gt.SyntaxKind.ExpressionStatement:
return visitNode(cbNode, (<gt.ExpressionStatement>node).expression);
case gt.SyntaxKind.IfStatement:
return visitNode(cbNode, (<gt.IfStatement>node).expression) ||
visitNode(cbNode, (<gt.IfStatement>node).thenStatement) ||
visitNode(cbNode, (<gt.IfStatement>node).elseStatement);
case gt.SyntaxKind.DoStatement:
return visitNode(cbNode, (<gt.DoStatement>node).statement) ||
visitNode(cbNode, (<gt.DoStatement>node).expression);
case gt.SyntaxKind.WhileStatement:
return visitNode(cbNode, (<gt.WhileStatement>node).expression) ||
visitNode(cbNode, (<gt.WhileStatement>node).statement);
case gt.SyntaxKind.ForStatement:
return visitNode(cbNode, (<gt.ForStatement>node).initializer) ||
visitNode(cbNode, (<gt.ForStatement>node).condition) ||
visitNode(cbNode, (<gt.ForStatement>node).incrementor) ||
visitNode(cbNode, (<gt.ForStatement>node).statement);
case gt.SyntaxKind.ContinueStatement:
case gt.SyntaxKind.BreakStatement:
case gt.SyntaxKind.BreakpointStatement:
break;
case gt.SyntaxKind.ReturnStatement:
return visitNode(cbNode, (<gt.ReturnStatement>node).expression);
case gt.SyntaxKind.IncludeStatement:
return visitNode(cbNode, (<gt.IncludeStatement>node).path);
}
}
class Diagnostic implements gt.Diagnostic {
file?: gt.SourceFile;
messageText: string;
code: number;
category: gt.DiagnosticCategory;
tags?: lsp.DiagnosticTag[];
source?: string;
start?: number;
length?: number;
line?: number;
col?: number;
constructor(file: gt.SourceFile, code: number, messageText: string, start: number, length: number) {
this.file = file;
this.code = code;
this.messageText = messageText;
this.start = start;
this.length = length;
}
toString() {
return `${this.file?.fileName} [${this.start}]: ${this.messageText}`.toString();
}
}
export function createFileDiagnostic(file: gt.SourceFile, start: number, length: number, message: gt.DiagnosticMessage): gt.Diagnostic {
// const end = start + length;
return <gt.Diagnostic>{
file: file,
code: message.code,
category: message.category,
start: start,
length: length,
messageText: message.message,
toString() {
return `${this.file?.fileName} [${this.start}]: ${this.messageText}`.toString();
}
};
}
export function createDiagnosticForNode(node: gt.Node, category: gt.DiagnosticCategory, msg: string, tags?: lsp.DiagnosticTag[]): gt.Diagnostic {
const d = <gt.Diagnostic>{
file: getSourceFileOfNode(node),
category: category,
start: node.pos,
length: node.end - node.pos,
line: node.line,
col: node.char,
messageText: msg,
toString() {
return `${this.file?.fileName} [${this.start}]: ${this.messageText}`.toString();
}
};
if (tags) {
d.tags = tags.concat();
}
return d;
}