plaxtony
Version:
Static code analysis of SC2 Galaxy Script
647 lines • 29 kB
JavaScript
;
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