UNPKG

plaxtony

Version:

Static code analysis of SC2 Galaxy Script

647 lines 29 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.createServer = exports.Server = exports.MetadataLoadLevel = void 0; const lsp = require("vscode-languageserver"); const vscode_languageserver_textdocument_1 = require("vscode-languageserver-textdocument"); const util = require("util"); const path = require("path"); const fs = require("fs-extra"); const utils_1 = require("../compiler/utils"); const store_1 = require("./store"); const utils_2 = require("./utils"); const provider_1 = require("./provider"); const diagnostics_1 = require("./diagnostics"); const navigation_1 = require("./navigation"); const completions_1 = require("./completions"); const signatures_1 = require("./signatures"); const definitions_1 = require("./definitions"); const hover_1 = require("./hover"); const references_1 = require("./references"); const rename_1 = require("./rename"); const archive_1 = require("../sc2mod/archive"); const timers_1 = require("timers"); const vscode_uri_1 = require("vscode-uri"); const common_1 = require("../common"); function translateNodeKind(node) { switch (node.kind) { case 141 /* VariableDeclaration */: const variable = node; const isConstant = variable.modifiers.some((value) => { return value.kind === 53 /* ConstKeyword */; }); return isConstant ? lsp.SymbolKind.Constant : lsp.SymbolKind.Variable; case 142 /* FunctionDeclaration */: return lsp.SymbolKind.Function; case 140 /* StructDeclaration */: return lsp.SymbolKind.Class; default: return lsp.SymbolKind.Field; } } function translateDeclaratons(origDeclarations) { const symbols = []; let kind; for (let node of origDeclarations) { const sourceFile = utils_1.findAncestor(node, (element) => { return element.kind === 127 /* SourceFile */; }); symbols.push({ kind: translateNodeKind(node), name: node.name.name, location: { uri: sourceFile.fileName, range: utils_2.getNodeRange(node) }, }); } return symbols; } function mapFromObject(stuff) { const m = new Map(); Object.keys(stuff).forEach((key) => { m.set(key, stuff[key]); }); return m; } const fileChangeTypeNames = { [lsp.FileChangeType.Created]: 'Created', [lsp.FileChangeType.Changed]: 'Changed', [lsp.FileChangeType.Deleted]: 'Deleted', }; var MetadataLoadLevel; (function (MetadataLoadLevel) { MetadataLoadLevel["None"] = "None"; MetadataLoadLevel["Core"] = "Core"; MetadataLoadLevel["Builtin"] = "Builtin"; MetadataLoadLevel["Default"] = "Default"; })(MetadataLoadLevel = exports.MetadataLoadLevel || (exports.MetadataLoadLevel = {})); class Server { constructor() { this.store = new store_1.Store(); this.documents = new lsp.TextDocuments(vscode_languageserver_textdocument_1.TextDocument); this.indexing = false; this.ready = false; this.documentUpdateRequests = new Map(); } createProvider(cls) { return provider_1.createProvider(cls, this.store); } createConnection() { this.connection = lsp.createConnection(); this.diagnosticsProvider = this.createProvider(diagnostics_1.DiagnosticsProvider); this.navigationProvider = this.createProvider(navigation_1.NavigationProvider); this.completionsProvider = this.createProvider(completions_1.CompletionsProvider); this.signaturesProvider = this.createProvider(signatures_1.SignaturesProvider); this.definitionsProvider = this.createProvider(definitions_1.DefinitionProvider); this.hoverProvider = this.createProvider(hover_1.HoverProvider); this.referenceProvider = this.createProvider(references_1.ReferencesProvider); this.renameProvider = this.createProvider(rename_1.RenameProvider); this.renameProvider.referencesProvider = this.referenceProvider; this.documents.listen(this.connection); this.documents.onDidChangeContent(this.onDidChangeContent.bind(this)); this.documents.onDidOpen(this.onDidOpen.bind(this)); this.documents.onDidClose(this.onDidClose.bind(this)); this.documents.onDidSave(this.onDidSave.bind(this)); this.connection.onDidChangeWatchedFiles(this.onDidChangeWatchedFiles.bind(this)); this.connection.onInitialize(this.onInitialize.bind(this)); this.connection.onInitialized(this.onInitialized.bind(this)); this.connection.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this)); this.connection.onCompletion(this.onCompletion.bind(this)); this.connection.onCompletionResolve(this.onCompletionResolve.bind(this)); this.connection.onDocumentSymbol(this.onDocumentSymbol.bind(this)); this.connection.onWorkspaceSymbol(this.onWorkspaceSymbol.bind(this)); this.connection.onSignatureHelp(this.onSignatureHelp.bind(this)); this.connection.onDefinition(this.onDefinition.bind(this)); this.connection.onHover(this.onHover.bind(this)); this.connection.onReferences(this.onReferences.bind(this)); 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; } log(msg, ...params) { this.connection.console.log(msg); if (params.length) { for (const p of params) { this.connection.console.log(util.inspect(p, { breakLength: 80, compact: false, })); } } } showErrorMessage(msg) { common_1.logger.error(`${msg}`); this.connection.window.showErrorMessage(msg); } async flushDocument(documentUri, isDirty = true) { if (!this.ready) { common_1.logger.info('Busy indexing..', { documentUri }); return false; } const req = this.documentUpdateRequests.get(documentUri); if (!req) return; if (req.promise) { await req.promise; } else { timers_1.clearTimeout(req.timer); req.isDirty = isDirty; await this.onUpdateContent(documentUri, req); } } async requestReindex() { if (this.indexing) return; const choice = await this.connection.window.showInformationMessage((`Workspace configuration has changed, reindex might be required. Would you like to do that now?`), { title: 'Yes', }, { title: 'No' }); if (!choice || choice.title !== 'Yes') return; await this.reindex(); } async reindex() { let archivePath; let workspace; // signal begining of indexing this.indexing = true; this.connection.sendNotification('indexStart'); this.store.clear(); let projFolders = await this.connection.workspace.getWorkspaceFolders(); if (!projFolders) projFolders = []; const archivePathToWsFolder = new Map(); // attempt to determine active document (archivePath) for non-empty project workspace if (projFolders.length) { const s2archives = []; if (this.config.archivePath) { if (path.isAbsolute(this.config.archivePath)) { if ((await fs.pathExists(archivePath))) { archivePath = this.config.archivePath; } else { this.showErrorMessage(`Specified archivePath '${this.config.archivePath}' resolved to '${archivePath}', but it doesn't exist.`); } } else { const archiveOsNormalPath = utils_2.osNormalizePath(this.config.archivePath); const candidates = (await Promise.all(projFolders.map(async (x) => { const testedPath = path.join(vscode_uri_1.default.parse(x.uri).fsPath, archiveOsNormalPath); const exists = await fs.pathExists(testedPath); if (exists) { return await fs.realpath(testedPath); } }))).filter(x => typeof x === 'string'); if (candidates.length) { archivePath = candidates[0]; common_1.logger.info(`Configured archivePath '${archiveOsNormalPath}' resolved to ${archivePath}`); if (candidates.length > 1) { common_1.logger.info(`Complete list of candidates:`, ...candidates); } } else { this.showErrorMessage(`Specified archivePath '${archiveOsNormalPath}' couldn't be found.`); } } } if (!archivePath) { const cfgFilesExclude = await this.connection.workspace.getConfiguration('files.exclude'); const excludePatterns = Object.entries(cfgFilesExclude).filter(x => x[1] === true).map(x => x[0]); common_1.logger.info('searching workspace for s2archives..'); common_1.logger.verbose('exclude patterns', ...excludePatterns); (await Promise.all(projFolders.map(async (wsFolder) => { return { wsFolder, foundArchivePaths: (await archive_1.findSC2ArchiveDirectories(vscode_uri_1.default.parse(wsFolder.uri).fsPath, { exclude: excludePatterns, })) }; }))).forEach(result => { for (const currArchivePath of result.foundArchivePaths) { archivePathToWsFolder.set(currArchivePath, result.wsFolder); } s2archives.push(...result.foundArchivePaths); }); common_1.logger.info('s2archives in workspace', ...s2archives); } if (!archivePath) { const s2maps = s2archives.filter(v => path.extname(v).toLowerCase() === '.sc2map'); common_1.logger.info(`s2maps in workspace`, ...s2maps); if (s2maps.length === 1) { archivePath = s2maps.pop(); } else if (s2archives.length === 1) { archivePath = s2archives.pop(); } else { this.connection.window.showInformationMessage(`Couldn't find applicable sc2map/sc2mod in the workspace. Set it manually under "sc2galaxy.archivePath" in projects configuration.`); } } } // build list of mod sources - directories with dependencies to lookup const modSources = []; if (this.config.dataPath) { if (path.isAbsolute(this.config.dataPath)) { modSources.push(this.config.dataPath); } else if (projFolders.length) { for (const wsFolder of projFolders) { const resolvedDataPath = path.join(vscode_uri_1.default.parse(wsFolder.uri).fsPath, this.config.dataPath); if (await fs.pathExists(resolvedDataPath)) { modSources.push(resolvedDataPath); } } } } if (!modSources.length) { modSources.push(this.initParams.initializationOptions.defaultDataPath); } modSources.push(...this.config.s2mod.sources); common_1.logger.info(`Workspace discovery`, ...modSources, { archivePath }); // setup s2workspace let wsArchive; if (archivePath) { const matchingWsFolder = archivePathToWsFolder.get(archivePath); const name = matchingWsFolder ? path.relative(vscode_uri_1.default.parse(matchingWsFolder.uri).fsPath, archivePath) : path.basename(archivePath); wsArchive = new archive_1.SC2Archive(name, archivePath); common_1.logger.info(`wsArchive`, wsArchive.name, wsArchive.directory, matchingWsFolder); } this.connection.sendNotification('indexProgress', `Resolving dependencies..`); const fallbackDep = new archive_1.SC2Archive(this.config.fallbackDependency, await archive_1.resolveArchiveDirectory(this.config.fallbackDependency, modSources)); const depLinks = await archive_1.resolveArchiveDependencyList(wsArchive ? wsArchive : fallbackDep, modSources, { overrides: mapFromObject(this.config.s2mod.overrides), fallbackResolve: async (depName) => { while (depName.length) { for (const wsFolder of projFolders) { const fsPath = vscode_uri_1.default.parse(wsFolder.uri).fsPath; if (depName.toLowerCase() === wsFolder.name.toLowerCase()) { return fsPath; } else { const result = await archive_1.resolveArchiveDirectory(depName, [fsPath]); if (result) return result; } } depName = depName.split('/').slice(1).join('/'); } return void 0; } }); for (const [name, src] of mapFromObject(this.config.s2mod.extra)) { if (!fs.existsSync(src)) { this.showErrorMessage(`Extra archive [${name}] '${src}' doesn't exist. Skipping.`); continue; } depLinks.list.push({ name: name, src: src }); } if (depLinks.unresolvedNames.length > 0) { this.showErrorMessage(`Some SC2 archives couldn't be found [${depLinks.unresolvedNames.map((s) => `'${s}'`).join(', ')}]. By a result certain intellisense capabilities might not function properly.`); } let depList = depLinks.list.map((item) => new archive_1.SC2Archive(item.name, item.src)); if (!wsArchive && !depList.find((item) => item.name === fallbackDep.name)) { depList.push(fallbackDep); } workspace = new archive_1.SC2Workspace(wsArchive, depList); common_1.logger.info('Resolved archives', ...workspace.allArchives.map(item => { return `${item.name} => ${item.directory}`; })); // process metadata files etc. this.connection.sendNotification('indexProgress', 'Indexing trigger libraries and data catalogs..'); await this.store.updateS2Workspace(workspace); await this.store.rebuildS2Metadata(this.config.metadata, this.config.dataCatalog); // process .galaxy files in the workspace this.connection.sendNotification('indexProgress', `Indexing Galaxy files..`); for (const modArchive of workspace.allArchives) { for (const extSrc of await modArchive.findFiles('**/*.galaxy')) { await this.syncSourceFile({ document: store_1.createTextDocumentFromFs(path.join(modArchive.directory, extSrc)) }); } } // almost done this.connection.sendNotification('indexProgress', 'Finalizing..'); // apply overdue updates to files issued during initial indexing for (const documentUri of this.documentUpdateRequests.keys()) { await this.flushDocument(documentUri); } // signal done this.indexing = false; this.ready = true; this.connection.sendNotification('indexEnd'); } onInitialize(params) { this.initParams = params; return { capabilities: { workspace: { workspaceFolders: { supported: true, changeNotifications: true, }, }, textDocumentSync: { change: lsp.TextDocumentSyncKind.Incremental, openClose: true, }, documentSymbolProvider: true, workspaceSymbolProvider: true, completionProvider: { triggerCharacters: ['.', '/'], resolveProvider: true, }, signatureHelpProvider: { triggerCharacters: ['(', ','], }, definitionProvider: true, hoverProvider: true, referencesProvider: true, renameProvider: { prepareProvider: true, }, } }; } onInitialized(params) { if (this.initParams.capabilities.workspace.workspaceFolders) { this.connection.workspace.onDidChangeWorkspaceFolders(this.onDidChangeWorkspaceFolders.bind(this)); } } onDidChangeConfiguration(ev) { const newConfig = ev.settings.sc2galaxy; let reindexRequired = false; let firstInit = !this.config; if (!this.config || this.config.archivePath !== newConfig.archivePath || this.config.dataPath !== newConfig.dataPath || this.config.fallbackDependency !== newConfig.fallbackDependency || (JSON.stringify(this.config.trace) !== JSON.stringify(newConfig.trace)) || (JSON.stringify(this.config.metadata) !== JSON.stringify(newConfig.metadata)) || (JSON.stringify(this.config.dataCatalog) !== JSON.stringify(newConfig.dataCatalog)) || (JSON.stringify(this.config.s2mod) !== JSON.stringify(newConfig.s2mod))) { common_1.logger.warn('Config changed, reindex required'); reindexRequired = true; } this.config = newConfig; switch (this.config.completion.functionExpand) { case "None": this.completionsProvider.config.functionExpand = 0 /* None */; break; case "Parenthesis": this.completionsProvider.config.functionExpand = 1 /* Parenthesis */; break; case "ArgumentsNull": this.completionsProvider.config.functionExpand = 2 /* ArgumentsNull */; break; case "ArgumentsDefault": this.completionsProvider.config.functionExpand = 3 /* ArgumentsDefault */; break; } this.referenceProvider.config = this.config.references; if (firstInit) { this.reindex(); } else if (reindexRequired) { this.requestReindex(); } } onDidChangeWorkspaceFolders(ev) { this.requestReindex(); } async onDidChangeContent(ev) { let req = this.documentUpdateRequests.get(ev.document.uri); if (req) { if (req.promise) { await req.promise; } else { if (req.timer) { timers_1.clearTimeout(req.timer); } this.documentUpdateRequests.delete(ev.document.uri); } req = null; } if (!req) { req = { content: ev.document.getText(), timer: null, promise: null, isDirty: true, version: ev.document.version, }; } if (!this.indexing && this.ready) { req.timer = timers_1.setTimeout(this.onUpdateContent.bind(this, ev.document.uri, req), this.config.documentUpdateDelay); } this.documentUpdateRequests.set(ev.document.uri, req); } async onUpdateContent(documentUri, req) { req.promise = new Promise((resolve) => { this.store.updateDocument({ uri: documentUri, getText: () => { return req.content; } }); this.documentUpdateRequests.delete(documentUri); const diagDelay = this.config.documentDiagnosticsDelay ? this.config.documentDiagnosticsDelay : 1; if (this.config.documentDiagnosticsDelay !== false || !req.isDirty) { timers_1.setTimeout(this.onDiagnostics.bind(this, documentUri, req), req.isDirty ? diagDelay : 1); } resolve(true); }); await req.promise; } onDiagnostics(documentUri, req) { if (this.documentUpdateRequests.has(documentUri)) return; if (this.documents.keys().indexOf(documentUri) === -1) return; if (this.documents.get(documentUri).version > req.version) return; if (this.store.isUriInWorkspace(documentUri)) { this.diagnosticsProvider.checkFile(documentUri); } this.connection.sendDiagnostics({ uri: documentUri, diagnostics: this.diagnosticsProvider.provideDiagnostics(documentUri), }); } onDidOpen(ev) { this.store.openDocuments.add(ev.document.uri); } onDidClose(ev) { this.store.openDocuments.delete(ev.document.uri); if (!this.store.isUriInWorkspace(ev.document.uri)) { this.store.removeDocument(ev.document.uri); common_1.logger.verbose('removed from store', ev.document.uri); } this.connection.sendDiagnostics({ uri: ev.document.uri, diagnostics: [], }); } async onDidSave(ev) { await this.flushDocument(ev.document.uri, true); } async onDidChangeWatchedFiles(ev) { for (const x of ev.changes) { const xUri = vscode_uri_1.default.parse(x.uri); if (xUri.scheme !== 'file') continue; if (xUri.fsPath.match(/sc2\w+\.(temp|orig)/gi)) continue; if (!this.store.isUriInWorkspace(x.uri)) continue; common_1.logger.verbose(`${fileChangeTypeNames[x.type]} '${x.uri}'`); switch (x.type) { case lsp.FileChangeType.Created: case lsp.FileChangeType.Changed: { if (!this.store.openDocuments.has(x.uri)) { this.syncSourceFile({ document: store_1.createTextDocumentFromUri(x.uri) }); } break; } case lsp.FileChangeType.Deleted: { this.store.removeDocument(x.uri); break; } } } } syncSourceFile(ev) { this.store.updateDocument(ev.document); } async onCompletion(params) { if (!this.store.documents.has(params.textDocument.uri)) return; await this.flushDocument(params.textDocument.uri); let context = null; try { if (this.initParams.capabilities.textDocument.completion.contextSupport) { context = params.context; } } catch (e) { } return this.completionsProvider.getCompletionsAt(params.textDocument.uri, utils_2.getPositionOfLineAndCharacter(this.store.documents.get(params.textDocument.uri), params.position.line, params.position.character), context); } onCompletionResolve(params) { return this.completionsProvider.resolveCompletion(params); } onDocumentSymbol(params) { if (!this.ready) return null; return translateDeclaratons(this.navigationProvider.getDocumentSymbols(params.textDocument.uri)); } onWorkspaceSymbol(params) { if (!this.ready) return null; return translateDeclaratons(this.navigationProvider.getWorkspaceSymbols(params.query)); } async onSignatureHelp(params) { if (!this.store.documents.has(params.textDocument.uri)) return null; await this.flushDocument(params.textDocument.uri); return this.signaturesProvider.getSignatureAt(params.textDocument.uri, utils_2.getPositionOfLineAndCharacter(this.store.documents.get(params.textDocument.uri), params.position.line, params.position.character)); } async onDefinition(params) { if (!this.store.documents.has(params.textDocument.uri)) return null; await this.flushDocument(params.textDocument.uri); return this.definitionsProvider.getDefinitionAt(params.textDocument.uri, utils_2.getPositionOfLineAndCharacter(this.store.documents.get(params.textDocument.uri), params.position.line, params.position.character)); } async onHover(params) { await this.flushDocument(params.textDocument.uri); return this.hoverProvider.getHoverAt(params); } async onReferences(params) { await this.flushDocument(params.textDocument.uri); return this.referenceProvider.onReferences(params); } async onRenameRequest(params) { await this.flushDocument(params.textDocument.uri); return this.renameProvider.onRenameRequest(params); } async onPrepareRename(params) { await this.flushDocument(params.textDocument.uri); const r = this.renameProvider.onPrepareRename(params); if (r && r.range) { timers_1.setTimeout(() => { this.onRenamePrefetch(params); }, 5); } return r; } async onRenamePrefetch(params) { await this.flushDocument(params.textDocument.uri); return this.renameProvider.prefetchLocations(); } async onDiagnoseDocumentRecursively(params) { await this.flushDocument(params.uri); const dtotal = this.diagnosticsProvider.checkFileRecursively(params.uri); return diagnostics_1.formatDiagnosticTotal(dtotal); } } __decorate([ common_1.logIt() ], Server.prototype, "reindex", null); __decorate([ common_1.logIt({ level: 'verbose', profiling: false, argsDump: true, resDump: true }) ], Server.prototype, "onInitialize", null); __decorate([ common_1.logIt({ level: 'verbose', profiling: false, argsDump: true }) ], Server.prototype, "onInitialized", null); __decorate([ common_1.logIt({ level: 'verbose', profiling: false, argsDump: ev => ev.settings.sc2galaxy }) ], Server.prototype, "onDidChangeConfiguration", null); __decorate([ common_1.logIt({ profiling: false, argsDump: true }) ], Server.prototype, "onDidChangeWorkspaceFolders", null); __decorate([ common_1.logIt({ level: 'verbose', profiling: false, argsDump: ev => { return { uri: ev.document.uri, ver: ev.document.version }; } }) ], Server.prototype, "onDidChangeContent", null); __decorate([ common_1.logIt({ level: 'verbose', argsDump: (docUri, req) => { return { uri: docUri, ver: req.version }; } }) ], Server.prototype, "onUpdateContent", null); __decorate([ common_1.logIt({ level: 'verbose', argsDump: (docUri, req) => { return { uri: docUri, ver: req.version }; } }) ], Server.prototype, "onDiagnostics", null); __decorate([ common_1.logIt({ level: 'verbose', profiling: false, argsDump: ev => ev.document.uri }) ], Server.prototype, "onDidOpen", null); __decorate([ common_1.logIt({ level: 'verbose', profiling: false, argsDump: ev => ev.document.uri }) ], Server.prototype, "onDidClose", null); __decorate([ common_1.logIt({ level: 'verbose', profiling: false, argsDump: ev => ev.document.uri }) ], Server.prototype, "onDidSave", null); __decorate([ common_1.logIt({ level: 'verbose', profiling: false }) ], Server.prototype, "onDidChangeWatchedFiles", null); __decorate([ common_1.logIt({ level: 'verbose', argsDump: (ev) => { return { uri: ev.document.uri, ver: ev.document.version }; } }) ], Server.prototype, "syncSourceFile", null); exports.Server = Server; function createServer() { return (new Server()).createConnection(); } exports.createServer = createServer; //# sourceMappingURL=server.js.map