From 3293e3fcd53dba1cf2830b726c65cfd70e432e33 Mon Sep 17 00:00:00 2001 From: Talv Date: Mon, 17 Sep 2018 10:39:50 +0200 Subject: [PATCH] add verify script command. that attempts to analyze the file respecting order of declarations --- src/compiler/binder.ts | 8 +- src/compiler/checker.ts | 91 +++++++++++++++++-- src/service/diagnostics.ts | 38 ++++++-- src/service/server.ts | 12 +++ src/service/store.ts | 6 +- .../simple/MapScript.galaxy | 9 ++ .../diagnostics_recursive/simple/sub_a.galaxy | 4 + tests/helpers.ts | 1 + tests/typechecker.ts | 13 ++- 9 files changed, 160 insertions(+), 22 deletions(-) create mode 100644 tests/fixtures/type_checker/diagnostics_recursive/simple/MapScript.galaxy create mode 100644 tests/fixtures/type_checker/diagnostics_recursive/simple/sub_a.galaxy diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index a8a6e85..8bbdbb8 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -2,7 +2,7 @@ import { Parser } from './parser'; import * as gt from './types'; import { SyntaxKind, SourceFile, Node, Symbol, SymbolTable, NamedDeclaration } from './types'; import { forEachChild, isNamedDeclarationKind, isDeclarationKind, isContainerKind, getSourceFileOfNode } from './utils'; -import { Store } from '../service/store'; +import { IStoreSymbols } from '../service/store'; // import { SignatureMeta, TypeChecker } from './checker'; export function getDeclarationName(node: Node): string { @@ -55,7 +55,7 @@ export function getDeclarationName(node: Node): string { // return result; // } -export function declareSymbol(node: gt.Declaration, store: Store, parentSymbol?: Symbol): Symbol { +export function declareSymbol(node: gt.Declaration, store: IStoreSymbols, parentSymbol?: Symbol): Symbol { let scopedSymbolTable: Symbol; let nodeSymbol: Symbol; let name: string; @@ -143,7 +143,7 @@ export function declareSymbol(node: gt.Declaration, store: Store, parentSymbol?: return nodeSymbol; } -export function bindSourceFile(sourceFile: SourceFile, store: Store) { +export function bindSourceFile(sourceFile: SourceFile, store: IStoreSymbols) { let currentScope: gt.Declaration; let currentContainer: gt.NamedDeclaration; @@ -182,7 +182,7 @@ export function bindSourceFile(sourceFile: SourceFile, store: Store) { } } -export function unbindSourceFile(sourceFile: SourceFile, store: Store) { +export function unbindSourceFile(sourceFile: SourceFile, store: IStoreSymbols) { function unbindSymbol(parentSymbol: Symbol) { for (const symbol of parentSymbol.members.values()) { symbol.declarations = symbol.declarations.filter((decl) => { diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 4773be0..c1593cc 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -1,11 +1,13 @@ import * as util from 'util'; +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 } from '../service/store'; import { tokenToString } from './scanner'; import { Printer } from './printer'; -import { declareSymbol } from './binder'; +import { declareSymbol, unbindSourceFile } from './binder'; import { getLineAndCharacterOfPosition } from '../service/utils'; let nextSymbolId = 1; @@ -614,15 +616,19 @@ function generateComplexTypes() { export class TypeChecker { private store: Store; private nodeLinks: gt.NodeLinks[] = []; - private diagnostics: gt.Diagnostic[] = []; + private diagnostics = new Map(); private currentSymbolContainer: gt.Symbol = null; + private currentDocuments = new Map(); constructor(store: Store) { this.store = store; + this.currentDocuments = this.store.documents; } private report(location: gt.Node, msg: string, category: gt.DiagnosticCategory = gt.DiagnosticCategory.Error): void { - this.diagnostics.push(createDiagnosticForNode(location, category, msg)); + const d = createDiagnosticForNode(location, category, msg); + const c = this.diagnostics.get(d.file.fileName); + if (c) c.push(d); } private getNodeLinks(node: gt.Node): gt.NodeLinks { @@ -809,21 +815,67 @@ export class TypeChecker { } public checkSourceFile(sourceFile: gt.SourceFile, bindSymbols = false) { - this.diagnostics = []; + this.diagnostics.clear(); + this.diagnostics.set(sourceFile.fileName, []); + this.currentDocuments = this.store.documents; if (bindSymbols) { - this.currentSymbolContainer = declareSymbol(sourceFile, this.store, null); + this.currentSymbolContainer = declareSymbol(sourceFile, {resolveGlobalSymbol: this.resolveGlobalSymbol.bind(this)}, null); } + sourceFile.statements.forEach(this.checkSourceElement.bind(this)); + 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 docUri = this.checkIncludeStatement(statement); + if (docUri && !this.currentDocuments.has(docUri)) { + const inclFile = this.store.documents.get(docUri); + if (inclFile) { + const currentSymbolContainer = this.currentSymbolContainer; + this.checkSourceFileRecursivelyWorker(inclFile); + this.currentSymbolContainer = currentSymbolContainer; + } + } + continue; + } this.checkSourceElement(statement); } - return this.diagnostics; + } + + public checkSourceFileRecursively(sourceFile: gt.SourceFile) { + this.diagnostics.clear(); + this.currentDocuments = new Map(); + + if (this.store.s2workspace) { + const coreMod = this.store.s2workspace.allArchives.find((archive) => archive.name === 'mods/core.sc2mod'); + if (coreMod) { + const fsp = path.join(coreMod.directory, 'base.sc2data', 'TriggerLibs', 'natives_missing.galaxy'); + const smNatives = this.store.documents.get(URI.file(fsp).toString()); + if (smNatives) { + this.checkSourceFileRecursivelyWorker(smNatives); + } + } + } + + this.checkSourceFileRecursivelyWorker(sourceFile); + + return { + success: Array.from(this.diagnostics.values()).findIndex((value, index) => value.length > 0) === -1, + diagnostics: this.diagnostics, + }; } private checkSourceElement(node: gt.Node) { let prevSymbolContainer = null; if (this.currentSymbolContainer && isDeclarationKind(node.kind)) { prevSymbolContainer = this.currentSymbolContainer; - this.currentSymbolContainer = declareSymbol(node, this.store, prevSymbolContainer); + 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) { @@ -859,6 +911,9 @@ export class TypeChecker { } switch (node.kind) { + case gt.SyntaxKind.IncludeStatement: + this.checkIncludeStatement(node); + break; case gt.SyntaxKind.Block: this.checkBlock(node); break; @@ -911,6 +966,22 @@ export class TypeChecker { } } + private checkIncludeStatement(node: gt.IncludeStatement) { + const path = node.path.value.replace(/\.galaxy$/i, '').toLowerCase(); + for (const docUri of this.store.documents.keys()) { + const meta = this.store.getDocumentMeta(docUri); + if (!meta.relativeName) continue; + if (meta.relativeName.toLowerCase() != path) continue; + const sourceFile = findAncestorByKind(node, gt.SyntaxKind.SourceFile); + if (this.store.documents.get(docUri) === sourceFile) { + this.report(node.path, `Self-include`, gt.DiagnosticCategory.Warning); + return; + } + return docUri; + } + this.report(node.path, `Given filename couldn't be matched`); + } + private checkFunction(node: gt.FunctionDeclaration) { this.checkSourceElement(node.type); @@ -1283,7 +1354,11 @@ export class TypeChecker { } } - for (const document of this.store.documents.values()) { + return this.resolveGlobalSymbol(name); + } + + private resolveGlobalSymbol(name: string) { + for (const document of this.currentDocuments.values()) { const symbol = document.symbol.members.get(name); if (symbol) { return symbol; diff --git a/src/service/diagnostics.ts b/src/service/diagnostics.ts index 803aa40..26de425 100644 --- a/src/service/diagnostics.ts +++ b/src/service/diagnostics.ts @@ -11,7 +11,7 @@ export type DiagnosticsCallback = (a: string) => void; export class DiagnosticsProvider extends AbstractProvider { private reporter?: DiagnosticsCallback; - private translateDiagnostics(sourceFile: gt.SourceFile, origDiagnostics: gt.Diagnostic[]): lsp.Diagnostic[] { + private translateDiagnostics(sourceFile: gt.SourceFile, origDiagnostics: gt.Diagnostic[], source: string): lsp.Diagnostic[] { let lspDiagnostics: lsp.Diagnostic[] = []; for (let dg of origDiagnostics) { @@ -22,6 +22,7 @@ export class DiagnosticsProvider extends AbstractProvider { end: getLineAndCharacterOfPosition(sourceFile, dg.start + dg.length) }, message: dg.messageText, + source, }); } @@ -39,17 +40,38 @@ export class DiagnosticsProvider extends AbstractProvider { } public provideDiagnostics(uri: string): lsp.Diagnostic[] { - let diagnostics: Diagnostic[] = []; const sourceFile = this.store.documents.get(uri); - const parseDiag = sourceFile.parseDiagnostics; - diagnostics = diagnostics.concat(parseDiag); + let parseDiag = sourceFile.parseDiagnostics; + let checkerDiag = sourceFile.additionalSyntacticDiagnostics; - const checkerDiag = sourceFile.additionalSyntacticDiagnostics; - diagnostics = diagnostics.concat(checkerDiag); + this.console.log(`${uri} - ${parseDiag.length} - ${checkerDiag.length}`); - this.console.info(`${uri} - ${parseDiag.length} - ${checkerDiag.length}`); + if (parseDiag.length > 100) parseDiag = parseDiag.slice(0, 100); + if (checkerDiag.length > 100) checkerDiag = checkerDiag.slice(0, 100); - return this.translateDiagnostics(sourceFile, diagnostics); + return [].concat( + this.translateDiagnostics(sourceFile, parseDiag, 'parser'), + this.translateDiagnostics(sourceFile, checkerDiag, 'typecheck') + ); + } + + public checkFileRecursively(documentUri: string) { + const checker = new TypeChecker(this.store); + const sourceFile = this.store.documents.get(documentUri); + const result = checker.checkSourceFileRecursively(sourceFile); + + const ld: lsp.PublishDiagnosticsParams[] = []; + for (const [itUri, itDg] of result.diagnostics) { + ld.push({ + uri: itUri, + diagnostics: this.translateDiagnostics(this.store.documents.get(itUri), itDg, 'verify'), + }) + } + + return { + success: result.success, + diagnostics: ld, + }; } } diff --git a/src/service/server.ts b/src/service/server.ts index 5635094..4743863 100644 --- a/src/service/server.ts +++ b/src/service/server.ts @@ -205,6 +205,8 @@ export class Server { this.connection.onRenameRequest(this.onRenameRequest.bind(this)); this.connection.onPrepareRename(this.onPrepareRename.bind(this)); + this.connection.onRequest('document/checkRecursively', this.onDiagnoseDocumentRecursively.bind(this)); + return this.connection; } @@ -612,6 +614,16 @@ export class Server { await this.flushDocument(params.textDocument.uri); return this.renameProvider.prefetchLocations(); } + + @wrapRequest() + private async onDiagnoseDocumentRecursively(params: lsp.TextDocumentIdentifier) { + await this.flushDocument(params.uri); + const dg = this.diagnosticsProvider.checkFileRecursively(params.uri); + for (const item of dg.diagnostics) { + this.connection.sendDiagnostics(item); + } + return dg.success; + } } export function createServer(): lsp.IConnection { diff --git a/src/service/store.ts b/src/service/store.ts index 70ec8c3..9cf51a9 100644 --- a/src/service/store.ts +++ b/src/service/store.ts @@ -145,7 +145,11 @@ export interface SourceFileMeta { archive?: SC2Archive; } -export class Store { +export interface IStoreSymbols { + resolveGlobalSymbol(name: string): gt.Symbol | undefined; +} + +export class Store implements IStoreSymbols { private parser = new Parser(); public rootPath?: string; public documents = new Map(); diff --git a/tests/fixtures/type_checker/diagnostics_recursive/simple/MapScript.galaxy b/tests/fixtures/type_checker/diagnostics_recursive/simple/MapScript.galaxy new file mode 100644 index 0000000..c68248d --- /dev/null +++ b/tests/fixtures/type_checker/diagnostics_recursive/simple/MapScript.galaxy @@ -0,0 +1,9 @@ +int ggg; + +include "sub_a.galaxy" + +int bbb; + +void InitMap() +{ +} diff --git a/tests/fixtures/type_checker/diagnostics_recursive/simple/sub_a.galaxy b/tests/fixtures/type_checker/diagnostics_recursive/simple/sub_a.galaxy new file mode 100644 index 0000000..2db5e64 --- /dev/null +++ b/tests/fixtures/type_checker/diagnostics_recursive/simple/sub_a.galaxy @@ -0,0 +1,4 @@ +void test_sub_a_stuff1() +{ + ggg = 1; +} diff --git a/tests/helpers.ts b/tests/helpers.ts index ca53759..3f1c96e 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -41,6 +41,7 @@ export function mockupStoreFromDirectory(directory: string) { return new Promise((resolve, reject) => { try { const store = new Store(); + store.rootPath = directory; const ws = new WorkspaceWatcher(directory); ws.onDidOpen((ev) => { store.updateDocument(ev.document); diff --git a/tests/typechecker.ts b/tests/typechecker.ts index b7b6a80..74ee823 100644 --- a/tests/typechecker.ts +++ b/tests/typechecker.ts @@ -4,11 +4,12 @@ import * as path from 'path'; import { assert } from 'chai'; import * as tc from '../src/compiler/checker'; import { TypeChecker } from '../src/compiler/checker'; -import { mockupStoreDocument, mockupStore, mockupSourceFile, mockupTextDocument } from './helpers'; +import { mockupStoreDocument, mockupStore, mockupSourceFile, mockupTextDocument, mockupStoreFromDirectory } from './helpers'; import { getPositionOfLineAndCharacter, findPrecedingToken, getTokenAtPosition } from '../src/service/utils'; import * as lsp from 'vscode-languageserver'; import * as gt from './../src/compiler/types'; import { unbindSourceFile } from '../src/compiler/binder'; +import URI from 'vscode-uri'; function getSymbolAt(checker: TypeChecker, sourceFile: gt.SourceFile, line: number, character: number): gt.Symbol | undefined { const token = getTokenAtPosition(getPositionOfLineAndCharacter(sourceFile, line, character), sourceFile); @@ -330,4 +331,14 @@ describe('Checker', () => { } }); }); + + describe('Diagnostics Recursive', () => { + it('simple', async () => { + const store = await mockupStoreFromDirectory(path.resolve('tests/fixtures/type_checker/diagnostics_recursive/simple')); + const checker = new TypeChecker(store); + const sourceFile = store.documents.get(URI.file(path.join(store.rootPath, 'MapScript.galaxy')).toString()); + const diagnostics = checker.checkSourceFileRecursively(sourceFile); + assert.isTrue(diagnostics.success); + }); + }); });