UNPKG

plaxtony

Version:

Static code analysis of SC2 Galaxy Script

447 lines 18.5 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.SC2Workspace = exports.S2ArchiveNsTypeKind = exports.S2ArchiveNsNameKind = exports.SC2Archive = exports.openArchiveWorkspace = exports.resolveArchiveDependencyList = exports.resolveArchiveDirectory = exports.LocalizationComponent = exports.CatalogComponent = exports.TriggerComponent = exports.Component = exports.findSC2ArchiveDirectories = exports.isSC2Archive = exports.BuiltinDeps = void 0; const vscode_languageserver_textdocument_1 = require("vscode-languageserver-textdocument"); const vscode_uri_1 = require("vscode-uri"); const fs = require("fs-extra"); const util = require("util"); const path = require("path"); const xml = require("xml2js"); const glob = require("fast-glob"); const trig = require("./trigger"); const cat = require("./datacatalog"); const loc = require("./localization"); const common_1 = require("../common"); const validArchiveExtensions = [ 'sc2map', 'sc2interface', 'sc2campaign', 'sc2mod', ]; const reValidArchiveExtension = new RegExp('\\.(' + validArchiveExtensions.join('|') + ')$', 'i'); var BuiltinDeps; (function (BuiltinDeps) { BuiltinDeps[BuiltinDeps["mods/core.sc2mod"] = 0] = "mods/core.sc2mod"; BuiltinDeps[BuiltinDeps["mods/glue.sc2mod"] = 1] = "mods/glue.sc2mod"; BuiltinDeps[BuiltinDeps["mods/liberty.sc2mod"] = 2] = "mods/liberty.sc2mod"; BuiltinDeps[BuiltinDeps["mods/swarm.sc2mod"] = 3] = "mods/swarm.sc2mod"; BuiltinDeps[BuiltinDeps["mods/void.sc2mod"] = 4] = "mods/void.sc2mod"; BuiltinDeps[BuiltinDeps["mods/libertymulti.sc2mod"] = 5] = "mods/libertymulti.sc2mod"; BuiltinDeps[BuiltinDeps["mods/swarmmulti.sc2mod"] = 6] = "mods/swarmmulti.sc2mod"; BuiltinDeps[BuiltinDeps["mods/voidmulti.sc2mod"] = 7] = "mods/voidmulti.sc2mod"; BuiltinDeps[BuiltinDeps["mods/balancemulti.sc2mod"] = 8] = "mods/balancemulti.sc2mod"; BuiltinDeps[BuiltinDeps["mods/starcoop/starcoop.sc2mod"] = 9] = "mods/starcoop/starcoop.sc2mod"; BuiltinDeps[BuiltinDeps["mods/war3.sc2mod"] = 10] = "mods/war3.sc2mod"; BuiltinDeps[BuiltinDeps["mods/novastoryassets.sc2mod"] = 11] = "mods/novastoryassets.sc2mod"; BuiltinDeps[BuiltinDeps["campaigns/liberty.sc2campaign"] = 12] = "campaigns/liberty.sc2campaign"; BuiltinDeps[BuiltinDeps["campaigns/swarm.sc2campaign"] = 13] = "campaigns/swarm.sc2campaign"; BuiltinDeps[BuiltinDeps["campaigns/void.sc2campaign"] = 14] = "campaigns/void.sc2campaign"; BuiltinDeps[BuiltinDeps["campaigns/libertystory.sc2campaign"] = 15] = "campaigns/libertystory.sc2campaign"; BuiltinDeps[BuiltinDeps["campaigns/swarmstory.sc2campaign"] = 16] = "campaigns/swarmstory.sc2campaign"; BuiltinDeps[BuiltinDeps["campaigns/voidstory.sc2campaign"] = 17] = "campaigns/voidstory.sc2campaign"; })(BuiltinDeps = exports.BuiltinDeps || (exports.BuiltinDeps = {})); const builtinDepsHierarchy = (function () { function depsFor(modName) { let list = []; let matches; if (matches = /^campaigns\/(liberty|swarm|void)story\.sc2campaign$/i.exec(modName)) { list.push('campaigns/' + matches[1] + '.sc2campaign'); } else if (matches = /^mods\/(liberty|swarm|void|balance)multi\.sc2mod$/i.exec(modName)) { if (matches[1] === 'balance') { list.push('mods/void.sc2mod'); } else { list.push('mods/' + matches[1] + '.sc2mod'); } } else if (matches = /^campaigns\/(liberty|swarm|void)\.sc2campaign$/i.exec(modName)) { if (matches[1] === 'void') { list.push('campaigns/swarm.sc2campaign'); } else if (matches[1] === 'swarm') { list.push('campaigns/liberty.sc2campaign'); } list.push('mods/' + matches[1] + '.sc2mod'); } else if (matches = /^mods\/(liberty|swarm|void)\.sc2mod$/i.exec(modName)) { if (matches[1] === 'void') { list.push('mods/swarm.sc2mod'); } else if (matches[1] === 'swarm') { list.push('mods/liberty.sc2mod'); } } else { switch (modName) { case 'mods/novastoryassets.sc2mod': case 'mods/starcoop/starcoop.sc2mod': { list.push('campaigns/void.sc2campaign'); break; } } } for (const item of list) { list = depsFor(item).concat(list.reverse()); } return list; } const depHierarchy = {}; for (const modName of Object.keys(BuiltinDeps).filter(v => typeof BuiltinDeps[v] === 'number')) { depHierarchy[modName] = []; if (modName !== 'mods/core.sc2mod') { depHierarchy[modName].push('mods/core.sc2mod'); } depHierarchy[modName] = depHierarchy[modName].concat(Array.from(new Set(depsFor(modName)))); } return depHierarchy; })(); function isSC2Archive(directory) { return path.basename(directory).match(reValidArchiveExtension); } exports.isSC2Archive = isSC2Archive; async function findSC2ArchiveDirectories(directory, opts = {}) { directory = path.resolve(directory); if (isSC2Archive(directory)) { return [directory]; } const results = (await glob(`**/*.{${validArchiveExtensions.join(',')}}`, { caseSensitiveMatch: false, absolute: true, cwd: directory, ignore: opts.exclude, onlyDirectories: true, stats: true, objectMode: true, })).map(x => x.path); return results.sort((a, b) => { return (validArchiveExtensions.indexOf(b.match(reValidArchiveExtension)[1].toLowerCase()) - validArchiveExtensions.indexOf(a.match(reValidArchiveExtension)[1].toLowerCase())); }); } exports.findSC2ArchiveDirectories = findSC2ArchiveDirectories; class Component { constructor(workspace) { this.workspace = workspace; } async load() { return await this.loadData(); } } exports.Component = Component; class TriggerComponent extends Component { constructor() { super(...arguments); this.store = new trig.TriggerStore(); } // protected libraries: trig.Library; async loadData() { const trigReader = new trig.XMLReader(this.store); for (const archive of this.workspace.metadataArchives) { for (const filename of await archive.findFiles('**/*.{TriggerLib,SC2Lib}')) { common_1.logger.debug(`:: ${archive.name}/${filename}`); this.store.addLibrary(await trigReader.loadLibrary(await archive.readFile(filename))); } if (await archive.hasFile('Triggers')) { common_1.logger.debug(`:: ${archive.name}/Triggers`); await trigReader.load(await archive.readFile('Triggers'), this.workspace.rootArchive !== archive); } } return true; } getStore() { return this.store; } } __decorate([ common_1.logIt() ], TriggerComponent.prototype, "loadData", null); exports.TriggerComponent = TriggerComponent; class CatalogComponent extends Component { constructor() { super(...arguments); this.store = new cat.CatalogStore(); } async loadData() { for await (const [archive, filename] of this.workspace.findFiles('**/GameData/**/*.xml')) { common_1.logger.debug(`:: ${archive.name}/${filename}`); const doc = await SC2Workspace.documentFromFile(archive, filename, 'xml'); this.store.update(doc, archive); } return true; } getStore() { return this.store; } } __decorate([ common_1.logIt() ], CatalogComponent.prototype, "loadData", null); exports.CatalogComponent = CatalogComponent; class LocalizationComponent extends Component { constructor() { super(...arguments); this.lang = 'enUS'; this.triggers = new loc.LocalizationTriggers(); this.strings = new Map(); } async loadStrings(name) { const textStore = new loc.LocalizationTextStore(); for (const archive of this.workspace.metadataArchives) { const filenames = await archive.findFiles('**/' + this.lang + '.SC2Data/LocalizedData/' + name + 'Strings.txt'); if (filenames.length) { common_1.logger.debug(`:: ${archive.name}/${filenames[0]}`); const locFile = new loc.LocalizationFile(); locFile.read(await archive.readFile(filenames[0])); textStore.merge(locFile); } } this.strings.set(name, textStore); } async loadData() { for (const archive of this.workspace.metadataArchives) { const filenames = await archive.findFiles('**/' + this.lang + '.SC2Data/LocalizedData/TriggerStrings.txt'); if (filenames.length) { common_1.logger.debug(`:: ${archive.name}/${filenames[0]}`); const locFile = new loc.LocalizationFile(); locFile.read(await archive.readFile(filenames[0])); this.triggers.merge(locFile); } } // await this.loadStrings('Trigger'); await this.loadStrings('Game'); await this.loadStrings('Object'); return true; } } __decorate([ common_1.logIt() ], LocalizationComponent.prototype, "loadData", null); exports.LocalizationComponent = LocalizationComponent; async function resolveArchiveDirectory(name, sources) { for (const src of sources) { const results = await glob(`**/${name}`, { caseSensitiveMatch: false, absolute: true, cwd: src, onlyDirectories: true, }); if (results.length) { return results[0]; } } } exports.resolveArchiveDirectory = resolveArchiveDirectory; async function resolveArchiveDependencyList(rootArchive, sources, opts = {}) { const list = []; const unresolvedNames = []; async function resolveWorker(archive) { for (const entry of await archive.getDependencyList()) { if (list.findIndex((item) => item.name === entry) !== -1) { continue; } const link = { name: entry, }; let dir; if (opts.overrides && opts.overrides.has(entry)) { dir = opts.overrides.get(entry); } else { dir = await resolveArchiveDirectory(entry, sources); } if (!dir && opts.fallbackResolve) { dir = await opts.fallbackResolve(entry); } if (dir) { await resolveWorker(new SC2Archive(entry, dir)); link.src = dir; list.push(link); } else { unresolvedNames.push(entry); } } } await resolveWorker(rootArchive); return { list, unresolvedNames, }; } exports.resolveArchiveDependencyList = resolveArchiveDependencyList; async function openArchiveWorkspace(archive, sources, overrides = null, extra = null) { const dependencyArchives = []; const result = await resolveArchiveDependencyList(archive, sources, { overrides, }); if (result.unresolvedNames.length > 0) { throw new Error(`couldn\'t resolve ${util.inspect(result.unresolvedNames)}\nSources: ${util.inspect(sources)}\nOverrides: ${util.inspect(overrides)}`); } for (const link of result.list) { dependencyArchives.push(new SC2Archive(link.name, link.src)); } if (extra) { for (const [name, src] of extra) { dependencyArchives.push(new SC2Archive(name, src)); } } return new SC2Workspace(archive, dependencyArchives); } exports.openArchiveWorkspace = openArchiveWorkspace; class SC2Archive { constructor(name = null, directory) { this.priority = 0; if (name === null) { name = path.basename(directory); } this.name = name.replace(/\\/g, '/').toLowerCase(); this.directory = fs.realpathSync(path.resolve(directory)); this.lcFsPath = this.directory.toLowerCase(); } async findFiles(pattern) { return glob(pattern, { cwd: this.directory, caseSensitiveMatch: false, onlyFiles: true, objectMode: false, ignore: [ 'base{0..99}.sc2maps/**', ], }); } async hasFile(filename) { return fs.pathExists(path.join(this.directory, filename)); } async readFile(filename) { return fs.readFile(path.join(this.directory, filename), 'utf8'); } relativePath(uri) { const fsPath = vscode_uri_1.default.parse(uri).fsPath; if (fsPath.substring(0, this.lcFsPath.length).toLowerCase() !== this.lcFsPath) return; const relativeFsPath = fsPath.substring(this.lcFsPath.length + 1); return relativeFsPath; } /** * returns lowercased and forward slash normalized list */ async getDependencyList() { let list = []; if (builtinDepsHierarchy[this.name]) { list = list.concat(builtinDepsHierarchy[this.name]); } if (await this.hasFile('DocumentInfo')) { try { const content = await this.readFile('DocumentInfo'); const data = await xml.parseStringPromise(content); for (const depValue of data.DocInfo.Dependencies[0].Value) { list.push(depValue.substr(depValue.indexOf('file:') + 5).replace(/\\/g, '/').toLowerCase()); } } catch (err) { common_1.logger.warn(`Couldn't read dependencies from "DocumentInfo" of "${this.name}`, err.message); list.push('mods/core.sc2mod'); } } return list; } get isBuiltin() { return builtinDepsHierarchy[this.name] !== void 0; } } exports.SC2Archive = SC2Archive; var S2ArchiveNsNameKind; (function (S2ArchiveNsNameKind) { S2ArchiveNsNameKind[S2ArchiveNsNameKind["base"] = 0] = "base"; S2ArchiveNsNameKind[S2ArchiveNsNameKind["enus"] = 1] = "enus"; // TODO: add missing localizations })(S2ArchiveNsNameKind = exports.S2ArchiveNsNameKind || (exports.S2ArchiveNsNameKind = {})); var S2ArchiveNsTypeKind; (function (S2ArchiveNsTypeKind) { S2ArchiveNsTypeKind[S2ArchiveNsTypeKind["sc2assets"] = 0] = "sc2assets"; S2ArchiveNsTypeKind[S2ArchiveNsTypeKind["sc2data"] = 1] = "sc2data"; })(S2ArchiveNsTypeKind = exports.S2ArchiveNsTypeKind || (exports.S2ArchiveNsTypeKind = {})); const reArchiveFileNs = /^(?:(?<nsName>[a-z]+)\.(?<nsType>(?:sc2data|sc2assets))(?:\/|\\))?(?<rp>.+)$/i; const reIsUri = /^[^:/?#]+:\/\//i; class SC2Workspace { constructor(rootArchive, dependencies = []) { this.allArchives = []; this.dependencies = []; this.metadataArchives = []; this.trigComponent = new TriggerComponent(this); this.locComponent = new LocalizationComponent(this); this.catalogComponent = new CatalogComponent(this); this.arvMap = new Map(); this.rootArchive = rootArchive; this.dependencies = dependencies; this.allArchives = this.allArchives.concat(this.dependencies); this.metadataArchives = this.allArchives; if (rootArchive) { this.allArchives.push(rootArchive); } this.allArchives.forEach((av, index) => { this.arvMap.set(av.name, av); av.priority = index * 20; }); } async *findFiles(pattern) { const stuff = this.allArchives.map(archive => [archive, archive.findFiles(pattern)]); for (const [archive, archiveFiles] of stuff) { for (const filename of await archiveFiles) { yield [archive, filename]; } } } static async documentFromFile(archive, filename, languageId) { if (!languageId) { languageId = path.extname(filename).split('.').pop(); } return vscode_languageserver_textdocument_1.TextDocument.create(vscode_uri_1.default.file(path.join(archive.directory, filename)).toString(), languageId, 0, await archive.readFile(filename)); } resolvePath(fsPath) { if (fsPath.match(reIsUri)) { fsPath = vscode_uri_1.default.parse(fsPath).fsPath; } for (const cArchive of this.allArchives) { if (!fsPath.toLowerCase().startsWith(cArchive.lcFsPath)) continue; const m = fsPath.substring(cArchive.directory.length + 1).match(reArchiveFileNs); if (!m) return; let priority = cArchive.priority; let ns; if (m.groups['nsName']) { ns = { name: m.groups['nsName'].toLowerCase(), type: m.groups['nsType'].toLowerCase(), }; priority += S2ArchiveNsTypeKind[ns.type]; if (ns.name !== 'base') { priority += 5; } } else { priority += 10; } const relativePath = m.groups['rp'].replace(/\\/g, '/'); return { fsPath: fsPath, relativePath: relativePath, archiveRelpath: m.groups['nsName'] ? `${m.groups['nsName']}.${m.groups['nsType']}/${relativePath}` : relativePath, namespace: ns, archive: cArchive, priority: priority, }; } } } exports.SC2Workspace = SC2Workspace; //# sourceMappingURL=archive.js.map