UNPKG

plaxtony

Version:

Static code analysis of SC2 Galaxy Script

1,346 lines (1,163 loc) 65.1 kB
import * as lsp from 'vscode-languageserver'; import * as path from 'path'; import URI from 'vscode-uri'; import * as gt from './types'; import { isComplexTypeKind } from '../compiler/utils'; import { isDeclarationKind, forEachChild, isPartOfExpression, isRightSideOfPropertyAccess, findAncestor, createDiagnosticForNode, isAssignmentOperator, isComparisonOperator, isReferenceKeywordKind, findAncestorByKind } from './utils'; import { Store, QualifiedSourceFile } from '../service/store'; import { tokenToString } from './scanner'; import { Printer } from './printer'; import { declareSymbol, unbindSourceFile } from './binder'; import { getLineAndCharacterOfPosition } from '../service/utils'; let nextSymbolId = 1; let nextNodeId = 1; const printer = new Printer(); export function getNodeId(node: gt.Node): number { if (!node.id) { node.id = nextNodeId; nextNodeId++; } return node.id; } export function getSymbolId(symbol: gt.Symbol): number { if (!symbol.id) { symbol.id = nextSymbolId; nextSymbolId++; } return symbol.id; } const enum CheckMode { Normal = 0, // Normal type checking SkipContextSensitive = 1, // Skip context sensitive function expressions } export abstract class AbstractType implements gt.Type { flags: gt.TypeFlags; symbol: gt.Symbol; public abstract isAssignableTo(target: AbstractType): boolean; public abstract isComparableTo(target: AbstractType): boolean; public abstract isBoolExpression(negation: boolean): boolean; public isValidBinaryOperation(operation: gt.BinaryOperator, rightType: AbstractType) { return false; } public isValidPrefixOperation(operation: gt.PrefixUnaryOperator) { return false; } public isValidPostfixOperation(operation: gt.PostfixUnaryOperator) { return false; } public getName(): string { return this.constructor.name; } } export class UnknownType extends AbstractType { flags: gt.TypeFlags = gt.TypeFlags.Unknown; public isAssignableTo(target: AbstractType) { return false; } public isComparableTo(target: AbstractType) { return false; } public isBoolExpression(negation: boolean) { return false; } } export class IntrinsicType extends AbstractType { readonly name: string; constructor(flags: gt.TypeFlags, name: string) { super(); this.flags = flags; this.name = name; } public isAssignableTo(target: AbstractType) { if (this === target) return true; if (target instanceof IntrinsicType) { if (target.flags & gt.TypeFlags.Fixed && (this.flags & gt.TypeFlags.Integer || this.flags & gt.TypeFlags.Byte)) return true; if (target.flags & gt.TypeFlags.Integer && (this.flags & gt.TypeFlags.Byte)) return true; if (target.flags & gt.TypeFlags.Byte && (this.flags & gt.TypeFlags.Integer)) return true; if (this.flags & gt.TypeFlags.Boolean && target.flags & gt.TypeFlags.Boolean) return true; } else if (target instanceof ComplexType) { if (this.flags & gt.TypeFlags.String && target.kind === gt.SyntaxKind.HandleKeyword) return true; } if (this.flags & gt.TypeFlags.Null && target.flags & gt.TypeFlags.Nullable) return true; return false; } public isComparableTo(target: AbstractType) { if (this === target) return true; if (target instanceof IntrinsicType) { if ( (this.flags & gt.TypeFlags.Integer || this.flags & gt.TypeFlags.Byte || this.flags & gt.TypeFlags.Fixed) && (target.flags & gt.TypeFlags.Integer || target.flags & gt.TypeFlags.Byte || target.flags & gt.TypeFlags.Fixed) ) { return true; } if (this.flags & gt.TypeFlags.Boolean && target.flags & gt.TypeFlags.Boolean) return true; } if (this.flags & gt.TypeFlags.Null && target.flags & gt.TypeFlags.Nullable) return true; return false; } public isBoolExpression(negation: boolean) { return true; } public isValidBinaryOperation(operation: gt.BinaryOperator, rightType: AbstractType) { if (this === rightType || (rightType instanceof LiteralType && rightType.value.kind === gt.SyntaxKind.StringLiteral)) { switch (operation) { case gt.SyntaxKind.PlusToken: if (this.flags & gt.TypeFlags.String) return true; } } if ( this === rightType || (rightType.flags & gt.TypeFlags.Numeric) || (rightType instanceof LiteralType && rightType.value.kind === gt.SyntaxKind.NumericLiteral) ) { switch (operation) { case gt.SyntaxKind.PlusToken: case gt.SyntaxKind.MinusToken: case gt.SyntaxKind.AsteriskToken: case gt.SyntaxKind.PercentToken: case gt.SyntaxKind.SlashToken: { if (this.flags & gt.TypeFlags.Numeric) { return true; } break; } case gt.SyntaxKind.AmpersandToken: case gt.SyntaxKind.BarToken: case gt.SyntaxKind.CaretToken: { // must be same type (either byte and byte, or int and int) if ((this.flags & gt.TypeFlags.IntLike) === (rightType.flags & gt.TypeFlags.IntLike)) { return true; } break; } case gt.SyntaxKind.LessThanLessThanToken: case gt.SyntaxKind.GreaterThanGreaterThanToken: { // no strict type checking, can implictly cast from byte to int if ((this.flags & gt.TypeFlags.IntLike) && (rightType.flags & gt.TypeFlags.IntLike)) { return true; } break; } // case gt.SyntaxKind.BarBarToken: // case gt.SyntaxKind.AmpersandAmpersandToken: // { // return false; // break; // } } } return false; } public isValidPrefixOperation(operation: gt.PrefixUnaryOperator) { switch (operation) { case gt.SyntaxKind.PlusToken: case gt.SyntaxKind.MinusToken: if (this.flags & gt.TypeFlags.Integer || this.flags & gt.TypeFlags.Byte || this.flags & gt.TypeFlags.Fixed) return true; case gt.SyntaxKind.TildeToken: if (this.flags & gt.TypeFlags.Integer || this.flags & gt.TypeFlags.Byte) return true; case gt.SyntaxKind.ExclamationToken: if (this.flags & gt.TypeFlags.Integer || this.flags & gt.TypeFlags.Byte || this.flags & gt.TypeFlags.Fixed || this.flags & gt.TypeFlags.Boolean || this.flags & gt.TypeFlags.String) return true; } } public isValidPostfixOperation(operation: gt.PostfixUnaryOperator) { return false; } public getName() { return this.name; } } export class ComplexType extends AbstractType implements gt.ComplexType { kind: gt.SyntaxKind; constructor(kind: gt.SyntaxKind) { super(); this.flags = gt.TypeFlags.Complex; this.kind = kind; switch (this.kind) { case gt.SyntaxKind.ColorKeyword: break; default: this.flags |= gt.TypeFlags.Nullable; break; } } get extendsHandle() { switch (this.kind) { case gt.SyntaxKind.AbilcmdKeyword: case gt.SyntaxKind.ActorKeyword: case gt.SyntaxKind.ActorscopeKeyword: case gt.SyntaxKind.AifilterKeyword: case gt.SyntaxKind.BankKeyword: case gt.SyntaxKind.BitmaskKeyword: case gt.SyntaxKind.CamerainfoKeyword: case gt.SyntaxKind.DatetimeKeyword: case gt.SyntaxKind.GenerichandleKeyword: case gt.SyntaxKind.EffecthistoryKeyword: case gt.SyntaxKind.MarkerKeyword: case gt.SyntaxKind.OrderKeyword: case gt.SyntaxKind.PlayergroupKeyword: case gt.SyntaxKind.PointKeyword: case gt.SyntaxKind.RegionKeyword: case gt.SyntaxKind.SoundKeyword: case gt.SyntaxKind.SoundlinkKeyword: case gt.SyntaxKind.TextKeyword: case gt.SyntaxKind.TimerKeyword: case gt.SyntaxKind.TransmissionsourceKeyword: case gt.SyntaxKind.UnitfilterKeyword: case gt.SyntaxKind.UnitgroupKeyword: case gt.SyntaxKind.UnitrefKeyword: case gt.SyntaxKind.WaveinfoKeyword: case gt.SyntaxKind.WavetargetKeyword: return true; default: return false; } } public isAssignableTo(target: AbstractType): boolean { if (this === target) return true; if (target instanceof ComplexType) { if (target.kind === gt.SyntaxKind.HandleKeyword) return this.extendsHandle; if (this.kind === gt.SyntaxKind.HandleKeyword) return target.extendsHandle; } else { if (this.kind === gt.SyntaxKind.HandleKeyword && target.flags & gt.TypeFlags.String) return true; } // if (target.flags && gt.TypeFlags.Null && this.flags & gt.TypeFlags.Nullable) return true; return false; } public isComparableTo(target: AbstractType) { if (this === target) return true; if (target.flags && gt.TypeFlags.Null && this.flags & gt.TypeFlags.Nullable) return true; return false; } public isBoolExpression(negation: boolean) { if (negation) { switch (this.kind) { case gt.SyntaxKind.TriggerKeyword: case gt.SyntaxKind.UnitKeyword: return false; } } return true; } public isValidBinaryOperation(operation: gt.BinaryOperator, rightType: AbstractType) { if (this !== rightType) return false; switch (operation) { case gt.SyntaxKind.PlusToken: { switch (this.kind) { case gt.SyntaxKind.TextKeyword: case gt.SyntaxKind.PointKeyword: return true; } break; } case gt.SyntaxKind.MinusToken: { switch (this.kind) { case gt.SyntaxKind.PointKeyword: return true; } break; } } return false; } public isValidPrefixOperation(operation: gt.PrefixUnaryOperator) { switch (operation) { case gt.SyntaxKind.ExclamationToken: return this.isBoolExpression(true); } return false; } public getName() { return tokenToString(this.kind); } } export class LiteralType extends AbstractType { value: gt.Literal; constructor(flags: gt.TypeFlags, value: gt.Literal) { super(); this.flags = flags; this.value = value; } public isAssignableTo(target: AbstractType) { if (this === target) return true; if (this.value.kind === gt.SyntaxKind.StringLiteral && target.flags & gt.TypeFlags.String) { return true; } if (this.value.kind === gt.SyntaxKind.NumericLiteral && ( target.flags & gt.TypeFlags.Byte || target.flags & gt.TypeFlags.Integer || target.flags & gt.TypeFlags.Fixed )) { if (this.value.text.indexOf('.') !== -1 && !(target.flags & gt.TypeFlags.Fixed)) { return false; } return true; } if (this.flags & gt.TypeFlags.Null && target.flags & gt.TypeFlags.Nullable) { return true; } return false; } public isComparableTo(target: AbstractType) { if (this === target) return true; if (this.value.kind === gt.SyntaxKind.NumericLiteral && ( target.flags & gt.TypeFlags.Byte || target.flags & gt.TypeFlags.Integer || target.flags & gt.TypeFlags.Fixed )) { return true; } if (target instanceof LiteralType && this.value.kind === target.value.kind) return true; return this.isAssignableTo(target); } public isBoolExpression(negation: boolean) { return true; } public isValidBinaryOperation(operation: gt.BinaryOperator, rightType: AbstractType) { let type: IntrinsicType; if (this.value.kind === gt.SyntaxKind.NumericLiteral) { if (this.flags & gt.TypeFlags.Fixed) { type = fixedType; } else { type = integerType; } } else if (this.value.kind === gt.SyntaxKind.StringLiteral) { type = stringType; } else { return false; } return type.isValidBinaryOperation(operation, rightType); } public isValidPrefixOperation(operation: gt.PrefixUnaryOperator) { let type: IntrinsicType; if (this.value.kind === gt.SyntaxKind.NumericLiteral) { if (this.flags & gt.TypeFlags.Fixed) { type = fixedType; } else { type = integerType; } } else if (this.value.kind === gt.SyntaxKind.StringLiteral) { type = stringType; } else { return false; } return type.isValidPrefixOperation(operation); } public getName() { let typeDesc = 'unknown'; if (this.flags & gt.TypeFlags.String) { typeDesc = 'string'; } else if (this.flags & gt.TypeFlags.Integer) { typeDesc = 'integer'; } else if (this.flags & gt.TypeFlags.Byte) { typeDesc = 'byte'; } else if (this.flags & gt.TypeFlags.Fixed) { typeDesc = 'fixed'; } else if (this.flags & gt.TypeFlags.Boolean) { typeDesc = 'bool'; } return `${this.value.text} [${typeDesc}]`; } } export class StructType extends AbstractType implements gt.StructType { symbol: gt.Symbol; constructor(symbol: gt.Symbol) { super(); this.flags = gt.TypeFlags.Struct; this.symbol = symbol; } public isAssignableTo(target: AbstractType) { if (target instanceof ReferenceType && target.kind === gt.SyntaxKind.StructrefKeyword && this.symbol === (<StructType>target.declaredType).symbol) { return true; } return false; } public isComparableTo(target: AbstractType) { if (this === target) return true; if (target instanceof StructType && target.symbol === this.symbol) return true; return false; } public isBoolExpression(negation: boolean) { return false; } public getName() { return this.symbol.escapedName; } } export class SignatureMeta { returnType: AbstractType; args: AbstractType[]; constructor(returnType: AbstractType, args: AbstractType[]) { this.returnType = returnType; this.args = args; } public match(other: SignatureMeta) { if (this.returnType !== other.returnType) return false; if (this.args.length !== other.args.length) return false; for (const [key, arg] of this.args.entries()) { if (this.args[key] !== arg) return false; } return true; } public toString() { const params = []; for (const p of this.args) { params.push(p.getName()); } return `${this.returnType.getName()} (${params.join(',')})`; } } export class FunctionType extends AbstractType implements gt.FunctionType { symbol: gt.Symbol; signature: SignatureMeta; constructor(symbol: gt.Symbol, signature: SignatureMeta) { super(); this.flags = gt.TypeFlags.Function; this.symbol = symbol; this.signature = signature; } public isAssignableTo(target: AbstractType): boolean { if (target instanceof ReferenceType && target.kind === gt.SyntaxKind.FuncrefKeyword) { if (!(target.declaredType.flags & gt.TypeFlags.Function)) return false; return this.isComparableTo((<FunctionType>target.declaredType)); } return false; } public isComparableTo(target: AbstractType): boolean { if (target instanceof ReferenceType && target.kind === gt.SyntaxKind.FuncrefKeyword) { if (!(target.declaredType.flags & gt.TypeFlags.Function)) return false; return this.isComparableTo((<FunctionType>target.declaredType)); } if (target instanceof FunctionType) { if (this.symbol === target.symbol) return true; if (this.signature.match(target.signature)) return true; } return false; } public isBoolExpression(negation: boolean): boolean { return false; } public getName(): string { return this.symbol.escapedName; } } export type ReferenceKind = gt.SyntaxKind.FuncrefKeyword | gt.SyntaxKind.StructrefKeyword | gt.SyntaxKind.ArrayrefKeyword; export class ReferenceType extends AbstractType { kind: ReferenceKind; declaredType: AbstractType; constructor(kind: ReferenceKind, declaredType: AbstractType) { super(); this.flags = gt.TypeFlags.Reference; this.kind = kind; this.declaredType = declaredType; if (this.kind === gt.SyntaxKind.FuncrefKeyword) { this.flags |= gt.TypeFlags.Nullable; } } public isAssignableTo(target: AbstractType): boolean { if (target instanceof ReferenceType && this.kind === target.kind) { return this.declaredType.isAssignableTo(target); } return false; } public isComparableTo(target: AbstractType) { if (target === nullType) return true; if (target instanceof ReferenceType && this.kind === target.kind) { return this.isAssignableTo(target); } if (target instanceof FunctionType) { if (this.kind === gt.SyntaxKind.FuncrefKeyword) { return (<FunctionType>this.declaredType).signature.match(target.signature); } } return false; } public isBoolExpression(negation: boolean) { return false; } public getName() { return tokenToString(this.kind) + '<' + this.declaredType.getName() + '>'; } } export class ArrayType extends AbstractType implements gt.ArrayType { elementType: AbstractType; constructor(elementType: AbstractType) { super(); this.flags = gt.TypeFlags.Array; this.elementType = elementType; } public isAssignableTo(target: AbstractType): boolean { if (target instanceof ReferenceType && target.kind === gt.SyntaxKind.ArrayrefKeyword) { // multi-dimensional array if (this.elementType instanceof ArrayType) { return this.getName() === target.declaredType.getName(); } // intrinsic type / whatever else if (this.elementType === (<ArrayType>target.declaredType).elementType) return true; } return false; } public isComparableTo(target: AbstractType) { return false; } public isBoolExpression(negation: boolean) { return false; } public getName() { return this.elementType.getName() + '[]'; } } export class TypedefType extends AbstractType implements gt.TypedefType { referencedType: AbstractType; constructor(referencedType: AbstractType) { super(); this.flags = gt.TypeFlags.Typedef; this.referencedType = referencedType; } public isAssignableTo(target: AbstractType) { return false; } public isComparableTo(target: AbstractType) { return false; } public isBoolExpression(negation: boolean) { return false; } public getName() { return this.referencedType.getName(); } } function createSymbol(flags: gt.SymbolFlags, name: string): gt.Symbol { const symbol = <gt.Symbol>{ flags: flags, escapedName: name, }; return symbol; } const unknownType = new UnknownType(); const nullType = new IntrinsicType(gt.TypeFlags.Null | gt.TypeFlags.Nullable, "null"); const boolType = new IntrinsicType(gt.TypeFlags.Boolean, "bool"); const trueType = new IntrinsicType(gt.TypeFlags.Boolean, "true"); const falseType = new IntrinsicType(gt.TypeFlags.Boolean, "false"); const stringType = new IntrinsicType(gt.TypeFlags.String | gt.TypeFlags.Nullable, "string"); const integerType = new IntrinsicType(gt.TypeFlags.Integer, "integer"); const byteType = new IntrinsicType(gt.TypeFlags.Byte, "byte"); const fixedType = new IntrinsicType(gt.TypeFlags.Fixed, "fixed"); const voidType = new IntrinsicType(gt.TypeFlags.Void, "void"); const complexTypes = generateComplexTypes(); function generateComplexTypes() { const map = new Map<gt.SyntaxKind, ComplexType>(); for (let i = gt.SyntaxKindMarker.FirstComplexType; i <= gt.SyntaxKindMarker.LastComplexType; i++) { const ckind = <gt.SyntaxKind>(<any>i); map.set(ckind, new ComplexType(ckind)); } return map; } export class TypeChecker { private store: Store; private nodeLinks: gt.NodeLinks[] = []; private diagnostics = new Map<string, gt.Diagnostic[]>(); private currentSymbolContainer: gt.Symbol = null; private currentSymbolReferences = new Map<gt.Symbol, Set<gt.Identifier>>(); private currentDocuments = new Map<string, gt.SourceFile>(); constructor(store: Store) { this.store = store; this.currentDocuments = this.store.documents; } private report(location: gt.Node, msg: string, category: gt.DiagnosticCategory = gt.DiagnosticCategory.Error, tags?: lsp.DiagnosticTag[]): void { const d = createDiagnosticForNode(location, category, msg, tags); const c = this.diagnostics.get(d.file.fileName); if (c) c.push(d); } private getNodeLinks(node: gt.Node): gt.NodeLinks { const nodeId = getNodeId(node); return this.nodeLinks[nodeId] || (this.nodeLinks[nodeId] = { flags: 0 }); } private checkTypeAssignableTo(source: AbstractType, target: AbstractType, node: gt.Node) { // TODO: error when using local var as reference if (source === unknownType || target === unknownType) return; if (!source.isAssignableTo(target)) { this.report(node, 'Type \'' + source.getName() + '\' is not assignable to type \'' + target.getName() + '\''); } } private checkTypeComparableTo(source: AbstractType, target: AbstractType, node: gt.Node) { if (source === unknownType || target === unknownType) return; if (!source.isComparableTo(target)) { this.report(node, 'Type \'' + source.getName() + '\' is not comparable to type \'' + target.getName() + '\''); } } private checkTypeBoolExpression(source: AbstractType, negation: boolean, node: gt.Node) { if (source === unknownType) return; if (!source.isBoolExpression(negation)) { this.report(node, 'Type \'' + source.getName() + '\' can not be used as boolean expression'); } } private getTypeFromArrayTypeNode(node: gt.ArrayTypeNode): ArrayType { const links = this.getNodeLinks(node); if (!links.resolvedType) { links.resolvedType = new ArrayType(this.getTypeFromTypeNode(node.elementType)); } return <ArrayType>links.resolvedType; } private getTypeFromMappedTypeNode(node: gt.MappedTypeNode): ReferenceType { const links = this.getNodeLinks(node); if (!links.resolvedType) { links.resolvedType = new ReferenceType( <ReferenceKind>node.returnType.kind, node.typeArguments.length ? this.getTypeFromTypeNode(node.typeArguments[0]) : unknownType ); } return <ReferenceType>links.resolvedType; } private resolveMappedReference(type: AbstractType) { if (type.flags & gt.TypeFlags.Reference) { type = (<ReferenceType>type).declaredType; } return type; } private getPropertyOfType(type: AbstractType, name: string): gt.Symbol | undefined { if (type && type.flags & gt.TypeFlags.Struct) { if (type.symbol.members.has(name)) { return type.symbol.members.get(name); } } } private getDeclaredTypeOfStruct(symbol: gt.Symbol) { // TODO: persist in map<symbol,type> return new StructType(symbol); } public getSignatureOfFunction(fnDecl: gt.FunctionDeclaration) { return new SignatureMeta( this.getTypeFromTypeNode(fnDecl.type), fnDecl.parameters.map((param) => { return this.getTypeFromTypeNode(param.type); }) ); } private getTypeOfFunction(symbol: gt.Symbol) { const fnDecl = <gt.FunctionDeclaration>symbol.declarations[0]; // TODO: persist in map<symbol,type> return new FunctionType(symbol, this.getSignatureOfFunction(fnDecl)); } private getTypeOfTypedef(symbol: gt.Symbol): AbstractType { const refType = this.getTypeFromTypeNode((<gt.TypedefDeclaration>symbol.declarations[0]).type); return new TypedefType(refType); } private getDeclaredTypeOfSymbol(symbol: gt.Symbol): AbstractType { if (symbol.flags & (gt.SymbolFlags.Struct)) { return this.getDeclaredTypeOfStruct(symbol); } else if (symbol.flags & (gt.SymbolFlags.Variable)) { return this.getTypeOfSymbol(symbol); } else if (symbol.flags & (gt.SymbolFlags.Function)) { // should we introduce SignatureType that describes fn declaration and return it instead? return this.getTypeOfFunction(symbol); } else if (symbol.flags & (gt.SymbolFlags.Typedef)) { return this.getTypeFromTypeNode((<gt.TypedefDeclaration>symbol.declarations[0]).type); } return unknownType; } private getTypeFromTypeNode(node: gt.TypeNode): AbstractType { switch (node.kind) { case gt.SyntaxKind.StringKeyword: return stringType; case gt.SyntaxKind.IntKeyword: return integerType; case gt.SyntaxKind.ByteKeyword: return byteType; case gt.SyntaxKind.FixedKeyword: return fixedType; case gt.SyntaxKind.BoolKeyword: return boolType; case gt.SyntaxKind.VoidKeyword: return voidType; case gt.SyntaxKind.NullKeyword: return nullType; // case gt.SyntaxKind.LiteralType: // return getTypeFromLiteralTypeNode(<LiteralTypeNode>node); case gt.SyntaxKind.ArrayType: return this.getTypeFromArrayTypeNode(<gt.ArrayTypeNode>node); case gt.SyntaxKind.MappedType: return this.getTypeFromMappedTypeNode(<gt.MappedTypeNode>node); case gt.SyntaxKind.Identifier: const symbol = this.getSymbolAtLocation(node); if (symbol) { return this.getDeclaredTypeOfSymbol(symbol); } else { return unknownType; } default: if (isComplexTypeKind(node.kind)) { return complexTypes.get(node.kind); } return unknownType; } } private getTypeOfSymbol(symbol: gt.Symbol): AbstractType { if ((symbol.flags & gt.SymbolFlags.Variable) || (symbol.flags & gt.SymbolFlags.Property)) { return this.getTypeOfVariableOrParameterOrProperty(symbol); } else if (symbol.flags & (gt.SymbolFlags.Function)) { return this.getTypeOfFunction(symbol); } else if (symbol.flags & (gt.SymbolFlags.Typedef)) { return this.getTypeOfTypedef(symbol); } return unknownType; } private getTypeOfVariableOrParameterOrProperty(symbol: gt.Symbol): AbstractType { return this.getTypeFromTypeNode((<gt.VariableDeclaration>symbol.declarations[0]).type); } public getTypeOfNode(node: gt.Node, followRef: boolean = false): AbstractType { // TODO: // if (isPartOfTypeNode(node)) { // return this.getTypeFromTypeNode(<TypeNode>node); // } if (isPartOfExpression(node)) { let type = this.getRegularTypeOfExpression(<gt.Expression>node); if (followRef) { type = this.resolveMappedReference(type); } return type; } return unknownType; } private getRegularTypeOfExpression(expr: gt.Expression): AbstractType { return this.getTypeOfExpression(expr); } private getTypeOfExpression(node: gt.Expression, cache?: boolean): AbstractType { return this.checkExpression(node); } private clear() { this.diagnostics.clear(); this.currentSymbolReferences.clear(); } public checkSourceFile(sourceFile: gt.SourceFile, bindSymbols = false) { this.clear(); this.diagnostics.set(sourceFile.fileName, []); this.currentDocuments = this.store.documents; if (bindSymbols) { unbindSourceFile(sourceFile, { resolveGlobalSymbol: this.resolveGlobalSymbol.bind(this) }); this.currentSymbolContainer = declareSymbol(sourceFile, { resolveGlobalSymbol: this.resolveGlobalSymbol.bind(this) }, null); } sourceFile.statements.forEach(this.checkSourceElement.bind(this)); if (bindSymbols) { this.checkForUnusedLocalDefinitions(this.currentSymbolContainer); } return Array.from(this.diagnostics.values()).pop(); } protected checkSourceFileRecursivelyWorker(sourceFile: gt.SourceFile) { unbindSourceFile(sourceFile, { resolveGlobalSymbol: this.resolveGlobalSymbol.bind(this) }); this.currentSymbolContainer = declareSymbol(sourceFile, { resolveGlobalSymbol: this.resolveGlobalSymbol.bind(this) }, null); this.diagnostics.set(sourceFile.fileName, []); this.currentDocuments.set(sourceFile.fileName, sourceFile); for (const statement of sourceFile.statements) { if (statement.kind === gt.SyntaxKind.IncludeStatement) { const qsFile = this.checkIncludeStatement(<gt.IncludeStatement>statement); if (qsFile && !this.currentDocuments.has(qsFile.fileName)) { const currentSymbolContainer = this.currentSymbolContainer; this.checkSourceFileRecursivelyWorker(qsFile); this.currentSymbolContainer = currentSymbolContainer; } continue; } this.checkSourceElement(statement); } } public checkSourceFileRecursively(sourceFile: gt.SourceFile) { this.clear(); this.currentDocuments = new Map<string, gt.SourceFile>(); if (this.store.s2workspace) { const coreMod = this.store.s2workspace.allArchives.find((archive) => archive.name === 'mods/core.sc2mod'); if (coreMod) { const corePath = path.join(coreMod.directory, 'base.sc2data', 'TriggerLibs'); const nativeScripts = [ path.join(corePath, 'natives_missing.galaxy'), path.join(corePath, 'natives.galaxy'), ]; for (const fpath of nativeScripts) { const qFile = this.store.documents.get(URI.file(fpath).toString()); if (qFile) { this.checkSourceFileRecursivelyWorker(qFile); } } } } this.checkSourceFileRecursivelyWorker(sourceFile); this.checkForIdentifierDefinitions(); this.checkForUnusedLocalDefinitions(this.currentSymbolContainer); return { success: Array.from(this.diagnostics.values()).findIndex((value, index) => value.length > 0) === -1, diagnostics: this.diagnostics, sourceFiles: <Map<string, QualifiedSourceFile>>this.currentDocuments, }; } private checkForIdentifierDefinitions() { for (const [symbol, symRef] of this.currentSymbolReferences) { if ((symbol.flags & gt.SymbolFlags.Function)) { if ((symbol.flags & gt.SymbolFlags.Native)) continue; if (!symRef.size) continue; if (symbol.valueDeclaration) continue; for (const identifier of symRef) { this.report(identifier, `Referenced function '${identifier.name}' hasn't been defined.`); } } } } private checkIsSymbolDeclarationDefined(symbol: gt.Symbol) { const symRef = this.currentSymbolReferences.get(symbol); if (symbol.flags & gt.SymbolFlags.Variable) { if (symRef && symRef.size > 1) return; if ( ((symbol.flags & gt.SymbolFlags.GlobalVariable) && !(symbol.flags & gt.SymbolFlags.Static)) || ((symbol.flags & gt.SymbolFlags.LocalVariable) && !(symbol.parent?.flags & gt.SymbolFlags.Function)) || ((symbol.flags & gt.SymbolFlags.FunctionParameter) && (symbol.parent?.flags & gt.SymbolFlags.Function)) ) { return; } for (const nodeDecl of symbol.declarations) { const nameDecl = <gt.NamedDeclaration>nodeDecl; if (nameDecl) { this.report( nameDecl.name, `'${symbol.escapedName}' variable is defined but never used.`, gt.DiagnosticCategory.Hint, [lsp.DiagnosticTag.Unnecessary] ); } } } } private checkForUnusedLocalDefinitions(rootSym: gt.Symbol) { if (!rootSym) return; for (const currSym of this.currentSymbolContainer.members.values()) { this.checkIsSymbolDeclarationDefined(currSym); if (currSym.flags & gt.SymbolFlags.Function && currSym.valueDeclaration) { for (const childSym of currSym.members.values()) { this.checkIsSymbolDeclarationDefined(childSym); // this.checkForUnusedLocalDefinitions(childSym); } } } } private checkSourceElement(node: gt.Node) { let prevSymbolContainer = null; if (this.currentSymbolContainer && isDeclarationKind(node.kind)) { prevSymbolContainer = this.currentSymbolContainer; this.currentSymbolContainer = declareSymbol(node, { resolveGlobalSymbol: this.resolveGlobalSymbol.bind(this) }, prevSymbolContainer); if (this.currentSymbolContainer.declarations.length > 1) { let previousDeclaration: gt.Declaration; if (node.kind === gt.SyntaxKind.FunctionDeclaration) { for (const pd of this.currentSymbolContainer.declarations) { if (pd === node) continue; if (pd.kind === gt.SyntaxKind.FunctionDeclaration && ( !(<gt.FunctionDeclaration>pd).body || !(<gt.FunctionDeclaration>node).body )) { continue; } previousDeclaration = pd; break; } } else if (node.kind === gt.SyntaxKind.ParameterDeclaration) { for (const pd of this.currentSymbolContainer.declarations) { if (pd === node) continue; if (pd.parent !== node.parent) continue; previousDeclaration = pd; break; } } else { previousDeclaration = this.currentSymbolContainer.declarations[this.currentSymbolContainer.declarations.length - 2]; } if (previousDeclaration) { const prevSourceFile = <gt.SourceFile>findAncestorByKind(previousDeclaration, gt.SyntaxKind.SourceFile); const prevPos = getLineAndCharacterOfPosition(prevSourceFile, previousDeclaration.pos); this.report((<gt.NamedDeclaration>node).name, `Symbol redeclared, previous declaration in ${prevSourceFile.fileName}:${prevPos.line + 1},${prevPos.character + 1}`); } } } switch (node.kind) { case gt.SyntaxKind.IncludeStatement: this.checkIncludeStatement(<gt.IncludeStatement>node); break; case gt.SyntaxKind.TypedefDeclaration: this.checkTypedefDeclaration(<gt.TypedefDeclaration>node); break; case gt.SyntaxKind.Block: this.checkBlock(<gt.Block>node); break; case gt.SyntaxKind.FunctionDeclaration: this.checkFunction(<gt.FunctionDeclaration>node); break; case gt.SyntaxKind.VariableDeclaration: this.checkVariableDeclaration(<gt.VariableDeclaration>node); break; case gt.SyntaxKind.PropertyDeclaration: this.checkPropertyDeclaration(<gt.PropertyDeclaration>node); break; case gt.SyntaxKind.ParameterDeclaration: this.checkParameterDeclaration(<gt.ParameterDeclaration>node); break; case gt.SyntaxKind.StructDeclaration: this.checkStructDeclaration(<gt.StructDeclaration>node); break; case gt.SyntaxKind.ExpressionStatement: this.checkExpressionStatement(<gt.ExpressionStatement>node); break; case gt.SyntaxKind.IfStatement: this.checkIfStatement(<gt.IfStatement>node); break; case gt.SyntaxKind.ForStatement: this.checkForStatement(<gt.ForStatement>node); break; case gt.SyntaxKind.WhileStatement: case gt.SyntaxKind.DoStatement: this.checkWhileStatement(<gt.WhileStatement>node); break; case gt.SyntaxKind.BreakStatement: case gt.SyntaxKind.ContinueStatement: this.checkBreakOrContinueStatement(<gt.BreakOrContinueStatement>node); break; case gt.SyntaxKind.ReturnStatement: this.checkReturnStatement(<gt.ReturnStatement>node); break; } if (prevSymbolContainer) { this.currentSymbolContainer = prevSymbolContainer; } } private checkIncludeStatement(node: gt.IncludeStatement) { let path = node.path.value.toLowerCase(); let segments = path.split('.'); if (segments.length > 1 && segments[segments.length - 1] !== 'galaxy') { this.report(node.path, `Dot in a script name is not allowed, unless path ends with ".galaxy"`, gt.DiagnosticCategory.Warning); } else { path = path.replace(/\.galaxy$/, ''); } const qsMap = this.store.qualifiedDocuments.get(path); if (!qsMap) { this.report(node.path, `Given filename couldn't be matched`); return; } const qsFile = Array.from(qsMap.values())[0]; const currCourceFile = <gt.SourceFile>findAncestorByKind(node, gt.SyntaxKind.SourceFile); if (currCourceFile === qsFile) { this.report(node, `Self-include`, gt.DiagnosticCategory.Warning); return; } return qsFile; } private checkTypedefDeclaration(node: gt.TypedefDeclaration) { this.checkDeclarationType(node.type); this.checkIdentifier(node.name); } private checkDeclarationType(node: gt.TypeNode) { switch (node.kind) { case gt.SyntaxKind.MappedType: return this.checkMappedType(<gt.MappedTypeNode>node); case gt.SyntaxKind.ArrayType: return this.checkArrayType(<gt.ArrayTypeNode>node); case gt.SyntaxKind.Identifier: return this.checkIdentifier(<gt.Identifier>node, false, false)[1]; } } private checkFunction(node: gt.FunctionDeclaration) { this.checkDeclarationType(node.type); this.checkIdentifier(node.name, false, false); this.checkTypeNoRefs(node.type); const currentSignature = this.getSignatureOfFunction(node); for (const prevDecl of node.symbol.declarations) { if (node === prevDecl) continue; if (prevDecl.kind !== gt.SyntaxKind.FunctionDeclaration) break; const previousSignature = this.getSignatureOfFunction(<gt.FunctionDeclaration>prevDecl); if (!currentSignature.match(previousSignature)) { this.report(node, `Function signature doesn't match it's previous declaration '${previousSignature.toString()}'`); break; } } node.parameters.forEach(this.checkSourceElement.bind(this)); if (node.body && node.body.kind === gt.SyntaxKind.Block) { const rtype = this.getTypeFromTypeNode(node.type); this.checkBlock(node.body); if (!(rtype.flags & gt.TypeFlags.Void) && !node.body.hasReturn) { this.report(node.name, 'Expected return statement'); } } } private checkLocalDeclaration(node: gt.ParameterDeclaration | gt.VariableDeclaration | gt.PropertyDeclaration, symbol: gt.Symbol) { const sourceFile = <gt.SourceFile>findAncestorByKind(node, gt.SyntaxKind.SourceFile); if ( (symbol.flags & gt.SymbolFlags.FunctionScopedVariable) || (symbol.flags & gt.SymbolFlags.Property) ) { const globalSym = this.resolveName(sourceFile, node.name.name, true); if (globalSym) { if ( (globalSym.flags & gt.SymbolFlags.Function) || (globalSym.flags & gt.SymbolFlags.Typedef) ) { const orgSourceFile = <gt.SourceFile>findAncestorByKind(globalSym.declarations[0], gt.SyntaxKind.SourceFile); const orgPos = getLineAndCharacterOfPosition(orgSourceFile, globalSym.declarations[0].pos); this.report( node.name, [ `Name '${node.name.name}' redefined. Already in use in global scope.`, `See: ${path.basename(URI.parse(orgSourceFile.fileName).fsPath)}:${orgPos.line + 1},${orgPos.character + 1}`, ].join(' ') ); } } } } private checkParameterDeclaration(node: gt.ParameterDeclaration) { const declType = this.checkDeclarationType(node.type); const [symbol, symType] = this.checkIdentifier(node.name); // const isNative = (<gt.FunctionDeclaration>node.parent).modifiers.some((value) => value.kind === gt.SyntaxKind.NativeKeyword); // if ((<gt.FunctionDeclaration>node.parent).body || isNative) { // } if (symbol) { this.checkLocalDeclaration(node, symbol); } const type = this.getTypeFromTypeNode(node.type); if (type instanceof StructType || type instanceof FunctionType) { this.report(node.type, 'Can only pass basic types'); } } private checkVariableDeclaration(node: gt.VariableDeclaration) { const declType = this.checkDeclarationType(node.type); const [symbol, symType] = this.checkIdentifier(node.name); if (node.initializer) { const varType = this.getTypeFromTypeNode(node.type); const exprType = this.checkExpression(node.initializer); this.checkTypeAssignableTo(exprType, varType, node.initializer); } const isConstant = node.modifiers?.some((value) => value.kind === gt.SyntaxKind.ConstKeyword); if (isConstant && declType instanceof TypedefType) { this.report(node.type, `Constant variables cannot reference Typedefs`); } if (symbol) { this.checkLocalDeclaration(node, symbol); } if (symbol.flags & gt.SymbolFlags.GlobalVariable) { this.checkTypeNoRefs(node.type); } } private checkTypeNoRefs(node: gt.TypeNode) { if (node.kind === gt.SyntaxKind.MappedType) { switch ((<gt.MappedTypeNode>node).returnType.kind) { case gt.SyntaxKind.StructrefKeyword: case gt.SyntaxKind.ArrayrefKeyword: { this.report(node, `Can not use arrayref/structref as a global, a field, or a return value (only as a local or a parameter).`); break; } } } } private checkPropertyDeclaration(node: gt.PropertyDeclaration) { const declType = this.checkDeclarationType(node.type); const [symbol, symType] = this.checkIdentifier(node.name); if (symbol) { this.checkLocalDeclaration(node, symbol); } this.checkTypeNoRefs(node.type); } private checkStructDeclaration(node: gt.StructDeclaration) { node.members.forEach(this.checkSourceElement.bind(this)); } private checkIfStatement(node: gt.IfStatement) { const exprType = this.checkExpression(node.expression); this.checkTypeBoolExpression(exprType, false, node.expression); this.checkSourceElement(node.thenStatement); if (node.elseStatement) { this.checkSourceElement(node.elseStatement); node.hasReturn = (<gt.Block>node.thenStatement).hasReturn && (<gt.Block>node.elseStatement).hasReturn; } } private checkForStatement(node: gt.ForStatement) { if (node.initializer) { this.checkExpression(node.initializer); } if (node.condition) { const exprType = this.checkExpression(node.condition); this.checkTypeBoolExpression(exprType, false, node.condition); } if (node.incrementor) { this.checkExpression(node.incrementor); } this.checkSourceElement(node.statement); } private checkWhileStatement(node: gt.WhileStatement) { if (node.expression) { const exprType = this.checkExpression(node.expression); this.checkTypeBoolExpression(exprType, false, node.expression); } this.checkSourceElement(node.statement); } private checkBreakOrContinueStatement(node: gt.BreakOrContinueStatement) { const loop = <gt.IterationStatement>findAncestor(node, (parent) => { switch (parent.kind) { case gt.SyntaxKind.ForStatement: case gt.SyntaxKind.WhileStatement: case gt.SyntaxKind.DoStatement: return true; } return false; }); if (!loop) { this.report(node, `${tokenToString(node.syntaxTokens[0].kind)} statement used outside of loop boundaries`); } } private checkReturnStatement(node: gt.ReturnStatement) { const fn = <gt.FunctionDeclaration>findAncestorByKind(node, gt.SyntaxKind.FunctionDeclaration); const rtype = this.getTypeFromTypeNode(fn.type); if (rtype.flags & gt.TypeFlags.Void && node.expression) { this.report(node, 'Unexpected value returned for void function'); } else if (!(rtype.flags & gt.TypeFlags.Void) && !node.expression) { this.report(node, 'Expected a return value'); } if (node.expression) { const exprType = this.checkExpression(node.expression); this.checkTypeAssignableTo(exprType, rtype, node.expression); } } private checkArrayType(node: gt.ArrayTypeNode) { this.checkExpression(node.size); this.checkDeclarationType(node.elementType); } private checkMappedType(node: gt.MappedTypeNode) { if (!isReferenceKeywordKind(node.returnType.kind)) { this.report(node.returnType, 'Invalid keyword for reference type provided - use funcref, arrayref or structref'); } if (node.typeArguments.length !== 1) { this.report(node, 'Expected exactly 1 argument'); } node.typeArguments.forEach(this.checkDeclarationType.bind(this)); if (node.typeArguments.length > 0) { const type = this.getTypeFromMappedTypeNode(node); let invalid = false; switch (type.kind) { case gt.SyntaxKind.StructrefKeyword: invalid = !(t