plaxtony
Version:
Static code analysis of SC2 Galaxy Script
641 lines (569 loc) • 25.6 kB
text/typescript
import * as gt from '../compiler/types';
import { SyntaxKind, Symbol, Node, SourceFile, FunctionDeclaration, NamedDeclaration, VariableDeclaration } from '../compiler/types';
import { TypeChecker } from '../compiler/checker';
import { AbstractProvider } from './provider';
import { tokenToString } from '../compiler/scanner';
import { findAncestor, isToken, isPartOfExpression, isKeywordKind } from '../compiler/utils';
import { getTokenAtPosition, findPrecedingToken, fuzzysearch, getAdjacentToken, getLineAndCharacterOfPosition } from './utils';
import { Printer } from '../compiler/printer';
import * as lsp from 'vscode-languageserver';
import { getDocumentationOfSymbol } from './s2meta';
import * as trig from '../sc2mod/trigger';
function isInComment(sourceFile: gt.SourceFile, pos: number) {
const comment = sourceFile.commentsLineMap.get(getLineAndCharacterOfPosition(sourceFile, pos).line);
return comment && pos >= comment.pos;
}
export const enum CompletionFunctionExpand {
None,
Parenthesis,
ArgumentsNull,
ArgumentsDefault,
};
export interface CompletionConfig {
functionExpand: CompletionFunctionExpand;
};
const enum CompletionItemDataFlags {
CanExpand = 1 << 1,
CanAppendSemicolon = 1 << 2,
};
interface CompletionItemData {
flags?: CompletionItemDataFlags;
parentSymbol?: string;
elementType?: 'gamelink';
gameType?: string;
};
export class CompletionsProvider extends AbstractProvider {
private printer: Printer = new Printer();
public config: CompletionConfig;
constructor() {
super();
this.config = <CompletionConfig>{
functionExpand: CompletionFunctionExpand.None,
};
}
public expandFunctionArguments(decl: gt.FunctionDeclaration): string[] {
let args: string[] = [];
let funcElement: trig.FunctionDef;
if (this.store.s2metadata && this.config.functionExpand === CompletionFunctionExpand.ArgumentsDefault) {
funcElement = <trig.FunctionDef>this.store.s2metadata.findElementByName(decl.name.name)
}
function isStringLikeParam(param: trig.Param) {
switch (param.valueType) {
case 'gamelink':
case 'string':
return true;
default:
return false;
}
}
for (const [key, param] of decl.parameters.entries()) {
let paramElement: trig.Param;
if (funcElement) {
const index = key - (funcElement.flags & trig.ElementFlag.Event ? 1 : 0);
if (index >= 0) {
const paramDef = funcElement.getParameters()[index];
if (paramDef.default) {
paramElement = paramDef.default.resolve();
}
}
}
if (!paramElement) {
if (param.type.kind === gt.SyntaxKind.IntKeyword || param.type.kind === gt.SyntaxKind.ByteKeyword) {
args.push(`0`);
}
else if (param.type.kind === gt.SyntaxKind.FixedKeyword) {
args.push(`0.0`);
}
else if (param.type.kind === gt.SyntaxKind.StringKeyword) {
args.push(`""`);
}
else if (param.type.kind === gt.SyntaxKind.BoolKeyword) {
args.push(`false`);
}
else {
args.push(`null`);
}
}
else {
if (paramElement.value) {
if (isStringLikeParam(paramElement)) {
args.push(`"${paramElement.value}"`);
}
else {
args.push(paramElement.value);
}
}
else if (paramElement.preset) {
const presetVal = paramElement.preset.resolve();
if (presetVal.value) {
args.push(presetVal.value);
}
else {
const presetDef = this.store.s2metadata.findPresetDef(presetVal);
if (presetDef) {
args.push(this.store.s2metadata.getNameOfPresetValue(presetDef, presetVal));
}
else {
args.push(this.store.s2metadata.getElementSymbolName(presetVal));
}
}
}
else if (paramElement.valueElement) {
args.push(this.store.s2metadata.getElementSymbolName(paramElement.valueElement.resolve().values[0].resolve()));
}
else if (paramElement.functionCall) {
const fcallDef = paramElement.functionCall.resolve().functionDef.resolve();
const fcallSymbol = this.store.resolveGlobalSymbol(
this.store.s2metadata.getElementSymbolName(fcallDef)
);
args.push(
this.store.s2metadata.getElementSymbolName(fcallDef)
+ '(' + this.expandFunctionArguments(<gt.FunctionDeclaration>fcallSymbol.declarations[0]).join(', ') + ')'
);
}
else {
args.push('null');
}
}
}
return args;
}
private buildFromSymbolDecl(symbol: Symbol): lsp.CompletionItem {
const node = <NamedDeclaration>(symbol.declarations[0]);
if (node.name === undefined) {
return;
}
const item = <lsp.CompletionItem>{
label: node.name.name,
};
switch (node.kind) {
case SyntaxKind.StructDeclaration:
item.kind = lsp.CompletionItemKind.Class;
break;
case SyntaxKind.FunctionDeclaration:
item.kind = lsp.CompletionItemKind.Function;
break;
case SyntaxKind.VariableDeclaration:
case SyntaxKind.ParameterDeclaration:
item.kind = lsp.CompletionItemKind.Variable;
break;
case SyntaxKind.PropertyDeclaration:
item.kind = lsp.CompletionItemKind.Property;
break;
case SyntaxKind.TypedefDeclaration:
item.kind = lsp.CompletionItemKind.Interface;
break;
default:
item.kind = lsp.CompletionItemKind.Text;
break;
}
return item;
}
private buildFromSymbolMembers(parentSymbol: Symbol, query?: string): lsp.CompletionItem[] {
const completions = <lsp.CompletionItem[]> [];
for (const symbol of parentSymbol.members.values()) {
if (!query || fuzzysearch(query, symbol.escapedName)) {
const item = this.buildFromSymbolDecl(symbol);
item.data = {
parentSymbol: parentSymbol.escapedName,
};
if (item) {
completions.push(item);
}
}
}
return completions;
}
private provideTriggerHandlers(): lsp.CompletionItem[] {
let completions = <lsp.CompletionItem[]> [];
for (const document of this.store.documents.values()) {
for (const [name, symbol] of document.symbol.members) {
if (symbol.declarations[0].kind !== gt.SyntaxKind.FunctionDeclaration) continue;
const funcDecl = <gt.FunctionDeclaration>symbol.declarations[0];
if (funcDecl.type.kind !== gt.SyntaxKind.BoolKeyword) continue;
if (funcDecl.parameters.length !== 2) continue;
if (funcDecl.parameters[0].type.kind !== gt.SyntaxKind.BoolKeyword) continue;
if (funcDecl.parameters[1].type.kind !== gt.SyntaxKind.BoolKeyword) continue;
const item = this.buildFromSymbolDecl(symbol);
completions.push(item);
}
}
return completions;
}
private provideGameLinks(gameType: string) {
const links = this.store.s2metadata.getGameLinkItem(gameType);
let completions = <lsp.CompletionItem[]> [];
for (const item of links) {
completions.push({
label: item.id,
data: {
elementType: 'gamelink',
gameType: gameType,
},
kind: lsp.CompletionItemKind.Value,
});
}
return completions;
}
private provideIncludes(query: string): lsp.CompletionList {
const completions = new Map<string, lsp.CompletionItem>();
for (const [relativeName, qsMap] of this.store.qualifiedDocuments) {
if (query && !relativeName.startsWith(query.toLowerCase())) continue;
const qsFiles = Array.from(qsMap.values()).filter(v => v.s2meta);
if (!qsFiles.length) continue;
const itemPart = qsFiles[0].s2meta.docName.substr(query.length).split('/');
let cpItem = completions.get(itemPart[0]);
if (!cpItem) {
cpItem = lsp.CompletionItem.create(itemPart[0]);
cpItem.documentation = '';
if (itemPart.length > 1) {
cpItem.kind = lsp.CompletionItemKind.Folder;
}
else {
cpItem.kind = lsp.CompletionItemKind.File;
cpItem.detail = qsFiles.filter(v => v.s2meta.file.archive).map(v => `${v.s2meta.file.archive.name}`).join(' | ');
cpItem.documentation = qsFiles.map(v => {
return (
(v.s2meta.file.archive ? `${v.s2meta.file.archive.name}/` : '') +
`${v.s2meta.file.relativePath}`
);
}).join('\n');
}
completions.set(cpItem.label, cpItem);
}
if (cpItem.kind === lsp.CompletionItemKind.Folder) {
const nDocs = qsFiles.filter(v => v.s2meta.file.archive).map(v => `${v.s2meta.file.archive.name}`);
if (nDocs.length) {
cpItem.documentation = Array.from(new Set(cpItem.documentation.toString().split('\n').concat(nDocs))).join('\n');
}
}
}
return {
items: Array.from(completions.values()),
isIncomplete: false,
};
}
public getCompletionsAt(uri: string, position: number, context?: lsp.CompletionContext): lsp.CompletionList {
let completions = <lsp.CompletionItem[]> [];
const sourceFile = this.store.documents.get(uri);
if (!sourceFile) return;
if (isInComment(sourceFile, position)) return;
let currentToken = findPrecedingToken(position, sourceFile);
// const adjacentToken = getAdjacentToken(position, sourceFile);
let filterBooleanKeywords = false;
// query
let query: string = null;
const processedSymbols = new Map<string, Symbol>();
if (currentToken && currentToken.pos <= position && currentToken.end >= position) {
const offset = position - currentToken.pos;
if (currentToken.kind === gt.SyntaxKind.Identifier) {
query = (<gt.Identifier>currentToken).name.substr(0, offset);
}
else if (isKeywordKind(currentToken.kind)) {
query = tokenToString(currentToken.kind).substr(0, offset);
}
}
// trigger handlers
if (currentToken && currentToken.kind === gt.SyntaxKind.StringLiteral && currentToken.parent.kind === gt.SyntaxKind.CallExpression) {
const callExpr = <gt.CallExpression>currentToken.parent;
if (
callExpr.expression.kind === gt.SyntaxKind.Identifier &&
["TriggerCreate", "TriggerFind"].find(x => x === (<gt.Identifier>(callExpr.expression)).name)
) {
return {
items: this.provideTriggerHandlers(),
isIncomplete: false,
};
}
}
// include
if (currentToken && currentToken.kind === gt.SyntaxKind.StringLiteral && currentToken.pos <= position && currentToken.end >= position && currentToken.parent.kind === gt.SyntaxKind.IncludeStatement) {
const inclStmt = <gt.IncludeStatement>currentToken.parent;
const offset = position - currentToken.pos;
query = (<gt.StringLiteral>currentToken).text.substr(1, offset - 1).replace(/(\/*)[^\/]+$/, '$1');
if ((<gt.StringLiteral>currentToken).text.match(/[^"]$/) || currentToken.end !== position) {
return this.provideIncludes(query);
}
}
if (context && context.triggerCharacter === '/') return;
// presets
if (this.store.s2metadata) {
const elementType = this.store.s2metadata.getElementTypeOfNode(currentToken);
// TODO: support <any> gamelink
if (
elementType &&
elementType.type === 'gamelink' &&
currentToken.kind === gt.SyntaxKind.StringLiteral &&
elementType.gameType
) {
return {
items: this.provideGameLinks(elementType.gameType),
isIncomplete: false,
};
}
if (elementType && elementType.type === 'preset') {
const tPreset = elementType.typeElement.resolve();
let matchingPresetCompletions = 0;
let totalPresetCompletions = 0;
switch (tPreset.baseType) {
default: {
let i = 0;
for (const name of this.store.s2metadata.getConstantNamesOfPreset(tPreset)) {
const symbol = this.store.resolveGlobalSymbol(name);
if (symbol) {
const citem = this.buildFromSymbolDecl(symbol);
if (!citem) continue;
citem.preselect = true;
citem.kind = lsp.CompletionItemKind.Enum;
citem.label = citem.label;
completions.push(citem);
processedSymbols.set(name, symbol);
totalPresetCompletions++;
if (!query || fuzzysearch(query, citem.label)) {
matchingPresetCompletions++;
}
}
}
break;
}
}
if (
(
context?.triggerKind === lsp.CompletionTriggerKind.Invoked &&
(!query || totalPresetCompletions === matchingPresetCompletions) &&
matchingPresetCompletions > 0
) ||
(
context?.triggerKind === lsp.CompletionTriggerKind.Invoked &&
((query?.length ?? 0) < 2 || totalPresetCompletions === matchingPresetCompletions) &&
matchingPresetCompletions > 0
)
) {
return {
items: completions,
isIncomplete: true,
};
}
else if (
(
context?.triggerKind === lsp.CompletionTriggerKind.TriggerForIncompleteCompletions &&
query?.length === 1 &&
matchingPresetCompletions > 0
)
) {
return {
items: completions,
isIncomplete: false,
};
}
}
}
// exit early for str and num literals
if (currentToken && (
currentToken.kind === gt.SyntaxKind.StringLiteral ||
currentToken.kind === gt.SyntaxKind.NumericLiteral
)) {
return {
items: completions,
isIncomplete: false,
};
}
// properties
if (currentToken) {
if (
(currentToken.kind === gt.SyntaxKind.DotToken || currentToken.kind === gt.SyntaxKind.Identifier) &&
(currentToken.parent.kind === gt.SyntaxKind.PropertyAccessExpression && (<gt.PropertyAccessExpression>currentToken.parent).expression !== currentToken)
) {
const checker = new TypeChecker(this.store);
currentToken = (<gt.PropertyAccessExpression>currentToken.parent).expression;
const type = checker.getTypeOfNode(currentToken, true);
if (type.flags & gt.TypeFlags.Struct) {
return {
items: this.buildFromSymbolMembers(type.symbol),
isIncomplete: false,
};
}
}
}
// local variables
if (currentToken) {
const currentContext = <FunctionDeclaration>findAncestor(currentToken, (element: Node): boolean => {
return element.kind === SyntaxKind.FunctionDeclaration;
})
if (currentContext) {
completions = completions.concat(this.buildFromSymbolMembers(currentContext.symbol, query));
}
}
// can append semicolon
let completionFlags: CompletionItemDataFlags = CompletionItemDataFlags.CanExpand;
if (currentToken) {
if (currentToken.kind === gt.SyntaxKind.Identifier && position < currentToken.end) {
completionFlags &= ~CompletionItemDataFlags.CanExpand;
}
if (currentToken.parent) {
switch (currentToken.parent.kind) {
case gt.SyntaxKind.ExpressionStatement: {
if (position >= currentToken.end) {
completionFlags |= CompletionItemDataFlags.CanAppendSemicolon;
}
else {
completionFlags |= currentToken.parent.syntaxTokens.findIndex(value => value.kind === gt.SyntaxKind.SemicolonToken) === -1 ? CompletionItemDataFlags.CanAppendSemicolon : 0;
}
break;
}
case gt.SyntaxKind.Block:
case gt.SyntaxKind.SourceFile:
{
if (position >= currentToken.end) {
completionFlags |= CompletionItemDataFlags.CanAppendSemicolon;
}
break;
}
case gt.SyntaxKind.FunctionDeclaration:
{
completionFlags &= ~CompletionItemDataFlags.CanExpand;
break;
}
}
}
}
let cpCount = 0;
let isIncomplete = false;
const cpLimit = 9000;
outer: for (const document of this.store.documents.values()) {
for (const [name, symbol] of document.symbol.members) {
if ((symbol.flags & gt.SymbolFlags.Static) && document.fileName !== uri) continue;
if (processedSymbols.has(name)) continue;
if (!query || fuzzysearch(query, name)) {
processedSymbols.set(name, symbol);
const citem = this.buildFromSymbolDecl(symbol);
citem.data = <CompletionItemData>{
flags: completionFlags,
};
completions.push(citem);
if (++cpCount >= cpLimit) {
if (context?.triggerKind !== lsp.CompletionTriggerKind.TriggerForIncompleteCompletions) {
isIncomplete = true;
}
break outer;
}
}
}
}
// keywords
for (let i: number = gt.SyntaxKindMarker.FirstKeyword; i <= gt.SyntaxKindMarker.LastKeyword; i++) {
const name = tokenToString(<any>i);
if (isIncomplete || !query || fuzzysearch(query, name)) {
completions.push({
label: name,
kind: lsp.CompletionItemKind.Keyword
});
}
}
return {
items: completions,
isIncomplete: isIncomplete,
};
}
public resolveCompletion(completion: lsp.CompletionItem): lsp.CompletionItem {
switch (completion.kind) {
case lsp.CompletionItemKind.Folder:
case lsp.CompletionItemKind.File:
{
return completion;
}
default:
{
break;
}
}
let symbol: gt.Symbol;
let parentSymbolName: string;
const customData: CompletionItemData = completion.data || {};
if (customData.elementType === 'gamelink') {
const localizedName = this.store.s2metadata.getGameLinkLocalizedName(customData.gameType, completion.label, true);
completion.detail = '';
if (localizedName) {
completion.detail += `"${localizedName}"`;
}
const linkDeclarations = Array.from(this.store.s2metadata.getGameLinkItem(customData.gameType, completion.label));
if (linkDeclarations.length > 0) {
const decl = linkDeclarations[0];
completion.detail += ` [${decl.ctype}]`;
completion.documentation = {
kind: lsp.MarkupKind.Markdown,
value: linkDeclarations.map(x => {
const details = this.store.s2metadata.getGameLinkDetails(x);
if (!details) return '`unknown`';
return `\`${details.archive.name}\` :: ${details.relativePath}`;
}).join('\\\n'),
};
}
return completion;
}
if (customData.parentSymbol) {
parentSymbolName = customData.parentSymbol;
}
for (const sourceFile of this.store.documents.values()) {
if (parentSymbolName) {
symbol = sourceFile.symbol.members.get(parentSymbolName);
if (!symbol) continue;
}
else {
symbol = sourceFile.symbol;
}
symbol = symbol.members.get(completion.label);
if (symbol) break;
}
if (
this.config.functionExpand !== CompletionFunctionExpand.None &&
completion.kind === lsp.CompletionItemKind.Function &&
customData.flags && customData.flags & CompletionItemDataFlags.CanExpand
) {
const decl = <gt.FunctionDeclaration>symbol.declarations[0];
let funcArgs: string[] = [];
// TODO: support funcrefs expansion
if (decl.kind === gt.SyntaxKind.FunctionDeclaration && this.config.functionExpand !== CompletionFunctionExpand.Parenthesis) {
funcArgs = this.expandFunctionArguments(decl);
}
if (funcArgs) {
completion.insertTextFormat = lsp.InsertTextFormat.Snippet;
funcArgs = funcArgs.map((item, index) => {
return `\${${index+1}:${item}}`;
});
completion.insertText = completion.label + '(' + funcArgs.join(', ') + ')';
}
else {
completion.insertTextFormat = lsp.InsertTextFormat.PlainText;
completion.insertText = completion.label + '($1)';
}
if (customData.flags && customData.flags & CompletionItemDataFlags.CanAppendSemicolon) {
completion.insertText += ';';
}
completion.insertText += '$0';
}
if (symbol) {
completion.documentation = getDocumentationOfSymbol(this.store, symbol, false);
let node = symbol.declarations[0];
switch (node.kind) {
case SyntaxKind.FunctionDeclaration:
node = Object.create(node);
(<gt.FunctionDeclaration>node).body = null;
// pass through
case SyntaxKind.VariableDeclaration:
case SyntaxKind.ParameterDeclaration:
case SyntaxKind.PropertyDeclaration:
case SyntaxKind.TypedefDeclaration:
completion.detail = this.printer.printNode(node);
break;
}
}
if (completion.documentation) {
completion.documentation = <lsp.MarkupContent>{
kind: lsp.MarkupKind.Markdown,
value: completion.documentation,
};
}
return completion;
}
}