UNPKG

plaxtony

Version:

Static code analysis of SC2 Galaxy Script

284 lines (241 loc) 9.6 kB
import * as gt from '../compiler/types'; import { SyntaxKind, SourceFile, Node, Symbol, SymbolTable } from '../compiler/types'; import { getSourceFileOfNode } from '../compiler/utils'; import { Parser } from '../compiler/parser'; import { S2WorkspaceMetadata } from './s2meta'; import { bindSourceFile, unbindSourceFile } from '../compiler/binder'; import { findSC2ArchiveDirectories, isSC2Archive, SC2Archive, SC2Workspace, openArchiveWorkspace, S2QualifiedFile } from '../sc2mod/archive'; import * as lsp from 'vscode-languageserver'; import { TextDocument } from 'vscode-languageserver-textdocument'; import * as path from 'path'; import * as fs from 'fs-extra'; import * as glob from 'fast-glob'; import URI from 'vscode-uri'; import { TypeChecker } from '../compiler/checker'; import { DataCatalogConfig, MetadataConfig } from './server'; export function createTextDocument(uri: string, text: string) { return TextDocument.create(uri, 'galaxy', 0, text); } export function createTextDocumentFromFs(filepath: string) { filepath = path.resolve(filepath); return createTextDocument(URI.file(filepath).toString(), fs.readFileSync(filepath, 'utf8')); } export async function readDocumentFile(fsPath: string) { return createTextDocument( URI.file(fsPath).toString(), await fs.readFile(fsPath, 'utf8') ); } export function createTextDocumentFromUri(uri: string) { return createTextDocument(uri, fs.readFileSync(URI.parse(uri).fsPath, 'utf8')); } export async function *openSourceFilesInLocation(...srcFolders: string[]) { const workspaceFolders = await Promise.all(srcFolders.map(async folder => { return { folder, galaxyFiles: await glob('**/*.galaxy', { cwd: folder, absolute: true, caseSensitiveMatch: false, onlyFiles: true, objectMode: false, }) }; })); for (const wsFolder of workspaceFolders) { for (const wsFile of wsFolder.galaxyFiles) { yield await readDocumentFile(wsFile); } } } export class IndexedDocument { textDocument: TextDocument; sourceNode: SourceFile; } export interface S2WorkspaceChangeEvent { src: string; workspace: SC2Workspace; } export class WorkspaceWatcher { public readonly folders: string[]; protected _onDidOpen = new lsp.Emitter<lsp.TextDocumentChangeEvent<TextDocument>>(); constructor(...folders: string[]) { this.folders = folders; } public get onDidOpen(): lsp.Event<lsp.TextDocumentChangeEvent<TextDocument>> { return this._onDidOpen.event; } } export class S2WorkspaceWatcher extends WorkspaceWatcher { protected _onDidOpenS2Workspace: lsp.Emitter<S2WorkspaceChangeEvent>; protected modSources: string[] = []; constructor(workspacePath: string, modSources: string[]) { super(workspacePath); this.modSources = modSources; this._onDidOpenS2Workspace = new lsp.Emitter<S2WorkspaceChangeEvent>(); } public get onDidOpenS2Archive(): lsp.Event<S2WorkspaceChangeEvent> { return this._onDidOpenS2Workspace.event; } public async watch() { const rootArchive = new SC2Archive(path.basename(this.folders[0]), this.folders[0]); const workspace = await openArchiveWorkspace(rootArchive, this.modSources); for (const modArchive of workspace.dependencies) { for (const extSrc of await modArchive.findFiles('**/*.galaxy')) { this._onDidOpen.fire({document: createTextDocumentFromFs(path.join(modArchive.directory, extSrc))}); } } for (const extSrc of await rootArchive.findFiles('**/*.galaxy')) { this._onDidOpen.fire({document: createTextDocumentFromFs(path.join(rootArchive.directory, extSrc))}); } this._onDidOpenS2Workspace.fire({ src: this.folders[0], workspace: workspace, }); } } export interface IStoreSymbols { resolveGlobalSymbol(name: string): gt.Symbol | undefined; } export interface SourceFileS2Meta { file: S2QualifiedFile; docName: string; } export interface QualifiedSourceFile extends SourceFile { s2meta?: SourceFileS2Meta; } export class Store implements IStoreSymbols { protected parser: Parser; public rootPath?: string; public documents = new Map<string, QualifiedSourceFile>(); public readonly qualifiedDocuments = new Map<string, Map<string, QualifiedSourceFile>>(); public openDocuments = new Set<string>(); public s2workspace: SC2Workspace; public s2metadata: S2WorkspaceMetadata; public constructor(opts: { parser?: Parser } = {}) { this.parser = opts.parser ? opts.parser : new Parser(); } public clear() { for (const key of this.documents.keys()) { if (this.openDocuments.has(key)) continue; this.removeDocument(key); } this.s2workspace = void 0; this.s2metadata = void 0; } protected removeQualifiedDocument(qsFile: QualifiedSourceFile) { const qDocMap = this.qualifiedDocuments.get(qsFile.s2meta.docName.toLowerCase()); if (qDocMap) { qDocMap.delete(qsFile.fileName); if (!qDocMap.size) { this.qualifiedDocuments.delete(qsFile.s2meta.docName.toLowerCase()); } } } protected requalifyFile(qsFile: QualifiedSourceFile) { if (qsFile.s2meta) { this.removeQualifiedDocument(qsFile); } const fsPath = URI.parse(qsFile.fileName).fsPath; let s2file: S2QualifiedFile; if (this.s2workspace) { s2file = this.s2workspace.resolvePath(fsPath); } else if (this.rootPath) { const commonBase = fsPath.toLowerCase().startsWith(this.rootPath.toLowerCase() + path.sep); if (commonBase) { const relativePath = fsPath.substring(this.rootPath.length + 1).toLowerCase(); s2file = { fsPath: fsPath, relativePath: relativePath, archiveRelpath: relativePath, priority: 0, }; } } if (s2file) { qsFile.s2meta = { file: s2file, docName: s2file.relativePath.replace(/\.galaxy$/, ''), }; let qDocMap = this.qualifiedDocuments.get(qsFile.s2meta.docName.toLowerCase()); if (!qDocMap) { qDocMap = new Map(); this.qualifiedDocuments.set(qsFile.s2meta.docName.toLowerCase(), qDocMap); } if (qDocMap.size > 1) { const tmpDocs = Array.from(qDocMap).sort((a, b) => a[1].s2meta.file.priority - b[1].s2meta.file.priority); qDocMap.clear(); for (const [k, v] of tmpDocs) { qDocMap.set(k, v); } } qDocMap.set(qsFile.fileName, qsFile); } else { qsFile.s2meta = void 0; } } public removeDocument(documentUri: string) { const currSorceFile = this.documents.get(documentUri); if (!currSorceFile) return; unbindSourceFile(currSorceFile, this); if (currSorceFile.s2meta) { this.removeQualifiedDocument(currSorceFile); } this.documents.delete(documentUri); } public updateDocument(document: TextDocument, check = false) { if (this.documents.has(document.uri)) { const currSorceFile = this.documents.get(document.uri); if (document.getText().length === currSorceFile.text.length && document.getText().valueOf() === currSorceFile.text.valueOf()) { return; } this.removeDocument(document.uri); } const sourceFile = this.parser.parseFile(document.uri, document.getText()); this.documents.set(document.uri, sourceFile); this.requalifyFile(sourceFile); if (check) { const checker = new TypeChecker(this); sourceFile.additionalSyntacticDiagnostics = checker.checkSourceFile(sourceFile, true); } else { bindSourceFile(sourceFile, this); } } public async updateS2Workspace(workspace: SC2Workspace) { this.s2workspace = workspace; this.qualifiedDocuments.clear(); for (const qsFile of this.documents.values()) { this.requalifyFile(qsFile); } } public async rebuildS2Metadata( metadataCfg: MetadataConfig = { loadLevel: 'Default', localization: 'enUS', }, dataCatalogConfig: DataCatalogConfig = { enabled: true, } ) { this.s2metadata = new S2WorkspaceMetadata(this.s2workspace, metadataCfg, dataCatalogConfig); await this.s2metadata.build(); } public isUriInWorkspace(documentUri: string) { const qsFile = this.documents.get(documentUri); if (qsFile && qsFile.s2meta) return true; const fsPath = URI.parse(documentUri).fsPath; const s2file = this.s2workspace.resolvePath(fsPath); if (s2file) return true; return false; } public resolveGlobalSymbol(name: string): gt.Symbol | undefined { for (const doc of this.documents.values()) { if (doc.symbol.members.has(name)) { return doc.symbol.members.get(name); } } } }