plaxtony
Version:
Static code analysis of SC2 Galaxy Script
381 lines (330 loc) • 13.6 kB
text/typescript
import * as gt from '../compiler/types';
import { getSourceFileOfNode } from '../compiler/utils';
import { Store } from './store';
import * as trig from '../sc2mod/trigger';
import * as cat from '../sc2mod/datacatalog';
import * as dtypes from '../sc2mod/dtypes';
import { SC2Workspace } from '../sc2mod/archive';
import { getLineAndCharacterOfPosition } from './utils';
import { logIt, logger } from '../common';
import { DataCatalogConfig, MetadataConfig } from './server';
const elementNotValidCharsRE = /[^a-zA-Z0-9_]+/g;
const elementValidCharsRE = /^[a-z][a-z0-9_]*$/i;
const quationMarkRE = /"/g;
const tildeRE = /~/g;
export class S2WorkspaceMetadata {
protected symbolMap: Map<string, trig.Element> = new Map();
protected presetValueParentMap: Map<string, trig.Preset> = new Map();
public getElementSymbolName(el: trig.Element) {
let parts: string[] = [];
let elemName: string = '';
if (el.name) {
elemName = el.name;
}
else {
const localizedName = this.workspace.locComponent.triggers.elementName('Name', el);
if (localizedName) {
elemName = localizedName.replace(elementNotValidCharsRE, '');
}
}
if (el instanceof trig.FunctionDef && (el.flags & trig.ElementFlag.Native || el.flags & trig.ElementFlag.NoScriptPrefix)) {
parts.push(elemName);
}
else if (
(<trig.PresetValue>el).value && (
(<trig.PresetValue>el).value.startsWith('c_') ||
(<trig.PresetValue>el).value === 'null' ||
(<trig.PresetValue>el).value === 'true' ||
(<trig.PresetValue>el).value === 'false'
)
) {
parts.push((<trig.PresetValue>el).value);
}
else {
if (el.libId) {
switch (el.constructor) {
case trig.FunctionDef:
case trig.Preset:
{
parts.push('lib' + el.libId);
break;
}
}
}
if (el instanceof trig.FunctionDef) {
if (el.flags & trig.ElementFlag.Operator) parts.push('op');
else parts.push('gf');
}
else if (el instanceof trig.Preset) {
parts.push('ge');
}
if (parts.length || el.constructor === trig.PresetValue) {
parts.push(elemName);
}
else {
parts.push(elemName.charAt(0).toLowerCase() + elemName.substr(1));
}
}
return parts.join('_');
}
private mapContainer(container: trig.ElementContainer) {
for (const el of container.getElements().values()) {
if (el instanceof trig.FunctionDef) {
if (el.flags & trig.ElementFlag.Template) continue;
this.symbolMap.set(this.getElementSymbolName(el), el);
}
else if (el instanceof trig.Preset) {
if (!(el.flags & trig.ElementFlag.PresetGenConstVar) && !(el.flags & trig.ElementFlag.PresetCustom)) continue;
if ((<trig.Preset>el).baseType === 'bool') continue;
for (const presetRef of (<trig.Preset>el).values) {
const presetValue = presetRef.resolve();
this.presetValueParentMap.set(presetValue.link(), el);
const pname = this.getNameOfPresetValue(el, presetValue);
if (!pname || pname === 'null' || !pname.match(elementValidCharsRE)) {
continue;
}
if (this.symbolMap.has(pname)) {
// logger.warn(
// `Already exists: "${pname}"`,
// [el.name, presetValue.name, presetValue.value],
// [this.symbolMap.get(pname)]
// );
continue;
}
this.symbolMap.set(pname, presetValue);
}
}
}
}
()
public async build() {
this.workspace.locComponent.lang = this.metadataCfg.localization;
this.workspace.metadataArchives = this.workspace.allArchives.filter(item => {
switch (this.metadataCfg.loadLevel) {
case 'Default':
{
return true;
}
case 'Builtin':
{
return item.isBuiltin;
}
case 'Core':
{
return item.name === 'mods/core.sc2mod';
}
case 'None':
default:
{
return false;
}
}
});
logger.info('metadata archives', ...this.workspace.metadataArchives.map(item => item.name));
const loaders: Promise<unknown>[] = [];
loaders.push(this.workspace.trigComponent.load());
loaders.push(this.workspace.locComponent.load());
if (this.dataCatalogConfig.enabled) {
loaders.push(this.workspace.catalogComponent.load());
}
await Promise.all(loaders);
for (const lib of this.workspace.trigComponent.getStore().getLibraries().values()) {
this.mapContainer(lib);
}
this.mapContainer(this.workspace.trigComponent.getStore());
}
constructor(
protected workspace: SC2Workspace,
protected metadataCfg: MetadataConfig,
protected dataCatalogConfig: DataCatalogConfig
) {
}
public findElementByName(name: string) {
return this.symbolMap.get(name);
}
public findPresetDef(presetValue: trig.PresetValue) {
return this.presetValueParentMap.get(presetValue.link());
}
public getNameOfPresetValue(preset: trig.Preset, presetValue: trig.PresetValue) {
if (preset.baseType === 'bool') return;
if (preset.flags & trig.ElementFlag.PresetCustom) {
return presetValue.value;
}
else {
return this.getElementSymbolName(preset) + '_' + this.getElementSymbolName(presetValue);
}
}
public getConstantNamesOfPreset(preset: trig.Preset) {
let names: string[] = [];
for (const link of preset.values) {
const presetValue = link.resolve();
const tmp = this.getNameOfPresetValue(preset, presetValue);
if (tmp) {
names.push(tmp);
}
}
return names;
}
public getParameterTypeDoc(el: trig.ParameterType) {
let typeName: string;
let type: string;
if (el.type === 'gamelink') {
type = `${el.type}<${(el.gameType || 'any')}>`;
}
else if (el.type === 'preset') {
typeName = this.workspace.locComponent.triggers.elementName('Name', el.typeElement.resolve());
type = `Preset<${this.getElementSymbolName(el.typeElement.resolve())}>`;
}
else {
type = el.type;
}
return { typeName, type};
}
public getParamDoc(el: trig.ParamDef) {
const name = this.workspace.locComponent.triggers.elementName('Name', el);
const type = this.getParameterTypeDoc(el.type).type;
return { name, type };
}
public getElementDoc(el: trig.Element, extended: boolean) {
let name = '**' + this.workspace.locComponent.triggers.elementName('Name', el) + '**';
if (el instanceof trig.FunctionDef) {
if (extended) {
const grammar = this.workspace.locComponent.triggers.elementName('Grammar', el);
if (grammar) {
name += ' (' + grammar.replace(tildeRE, '`') + ')';
}
}
if (el.flags & trig.ElementFlag.Restricted) {
name += '\n\n__Blizzard only__';
}
const hint = this.workspace.locComponent.triggers.elementName('Hint', el);
if (hint) {
name += '\n\n' + hint.replace(quationMarkRE, '*');
}
return name;
}
else if (el instanceof trig.PresetValue) {
const presetName = this.workspace.locComponent.triggers.elementName('Name', this.findPresetDef(el));
return name + (presetName ? ' - ' + presetName : '');
}
else if (el instanceof trig.ParamDef) {
let type: string;
if ((<trig.ParamDef>el).type.type === 'gamelink') {
type = '`gamelink<' + ((<trig.ParamDef>el).type.gameType || 'any') + '>`';
}
else if ((<trig.ParamDef>el).type.type === 'preset') {
type = '' + this.workspace.locComponent.triggers.elementName('Name', (<trig.ParamDef>el).type.typeElement.resolve()) + '';
}
else {
type = '`' + (<trig.ParamDef>el).type.type + '`';
}
return name + ' - ' + type + '';
}
else {
return name;
}
}
public getSymbolDoc(symbolName: string, extended: boolean = true) {
const el = this.findElementByName(symbolName);
if (!el) return null;
return this.getElementDoc(el, extended);
}
public getFunctionArgumentsDoc(symbolName: string) {
const el = <trig.FunctionDef>this.findElementByName(symbolName);
if (!el) return null;
const docs: string[] = [];
if (el.flags & trig.ElementFlag.Event) {
docs.push('**Trigger**');
}
for (const param of el.getParameters()) {
docs.push(this.getElementDoc(param, false));
}
return docs;
}
public getElementTypeOfNode(node: gt.Node) {
// if (node.kind !== gt.SyntaxKind.StringLiteral) return null;
if (node.parent.kind !== gt.SyntaxKind.CallExpression) return null;
const callExpr = <gt.CallExpression>node.parent;
if (callExpr.expression.kind !== gt.SyntaxKind.Identifier) return null;
const el = <trig.FunctionDef>this.findElementByName((<gt.Identifier>callExpr.expression).name);
if (!el) return null;
let index: number = null;
if (node.kind === gt.SyntaxKind.CommaToken || node.kind === gt.SyntaxKind.OpenParenToken) {
for (const [key, token] of callExpr.syntaxTokens.entries()) {
index = key - 1;
if (node.end < token.end) {
break;
}
}
}
else {
for (const [key, arg] of callExpr.arguments.entries()) {
if (arg === node) {
index = key;
break;
}
}
}
if (index === null) return null;
if (el.flags & trig.ElementFlag.Event) {
index--;
}
if (el.getParameters().length <= index || index < 0) return null;
return el.getParameters()[index].type;
}
public getGameLinkItem(gameType: string, id?: string) {
const family = (dtypes as any).S2DataCatalogDomain[gameType];
let results = Array
.from(this.workspace.catalogComponent.getStore().findEntry(family))
.map(x => Array.from(x))
.flat()
;
if (id) {
results = results.filter(x => x.id === id);
}
return new Set(results);
}
public getGameLinkDetails(entity: cat.CatalogDeclaration) {
return this.workspace.resolvePath(entity.uri);
}
public getGameLinkLocalizedName(gameType: string, gameLink: string, includePrefix: boolean = false) {
const name = (
this.workspace.locComponent.strings.get('Game').text(`${gameType}/Name/${gameLink}`) ??
this.workspace.locComponent.strings.get('Object').text(`${gameType}/Name/${gameLink}`)
);
if (!includePrefix) {
return name;
}
if (!name) {
return undefined;
}
const prefix = this.workspace.locComponent.strings.get('Object').text(`${gameType}/EditorPrefix/${gameLink}`);
const suffix = this.workspace.locComponent.strings.get('Object').text(`${gameType}/EditorSuffix/${gameLink}`);
return (prefix ? prefix + ' ' : '') + (name ?? gameLink) + (suffix ? ' ' + suffix : '');
}
}
export function getDocumentationOfSymbol(store: Store, symbol: gt.Symbol, extended: boolean = true) {
if (store.s2metadata) {
const r = store.s2metadata.getSymbolDoc(symbol.escapedName, extended);
if (r) return r;
}
for (const decl of symbol.declarations) {
const sourceFile = getSourceFileOfNode(decl);
const linesTxt: string[] = [];
let currLine = decl.line;
if (!sourceFile.commentsLineMap.has(currLine)) {
--currLine;
}
while (currLine > 0 && sourceFile.commentsLineMap.has(currLine)) {
const ctoken = sourceFile.commentsLineMap.get(currLine);
const cpos = getLineAndCharacterOfPosition(sourceFile, ctoken.pos);
if (ctoken.line !== decl.line && cpos.character > 0) break;
linesTxt.push(sourceFile.text.substring(ctoken.pos + 2, ctoken.end));
--currLine;
}
if (linesTxt.length) {
return linesTxt.reverse().map((line) => line.replace(/^ /, '')).join(' \n');
}
}
return null;
}