diff --git a/package.json b/package.json index c186046..586bcb4 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "lint": "eslint ./client/src ./server/src --ext .ts,.tsx", "postinstall": "cd client && npm install && cd ../server && npm install && cd ..", "test": "sh ./scripts/e2e.sh", - "test:server": "cd server && npx mocha -r ts-node/register src/test/**/*.test.ts" + "test:server": "cd server && npx mocha -r ts-node/register src/test/**/*.test.ts src/util/test/**/*.test.ts" }, "devDependencies": { "@types/mocha": "^10.0.6", diff --git a/server/src/analyzer.ts b/server/src/analyzer.ts index 93a2859..3fba16b 100644 --- a/server/src/analyzer.ts +++ b/server/src/analyzer.ts @@ -42,7 +42,7 @@ export default class Analyzer { logger.debug(tree.rootNode.toString()); // Get declarations - const declarations = getAllDeclarationsInTree({ tree, uri }); + const declarations = getAllDeclarationsInTree(tree, uri); // Update saved analysis for document uri this.uriToAnalyzedDocument[uri] = { @@ -59,13 +59,13 @@ export default class Analyzer { * * TODO: convert to DocumentSymbol[] which is a hierarchy of symbols found in a given text document. */ - public getDeclarationsForUri({ uri }: { uri: string }): LSP.SymbolInformation[] { + public getDeclarationsForUri(uri: string): LSP.SymbolInformation[] { const tree = this.uriToAnalyzedDocument[uri]?.tree; if (!tree?.rootNode) { return []; } - return getAllDeclarationsInTree({ uri, tree }); + return getAllDeclarationsInTree(tree, uri); } } diff --git a/server/src/server.ts b/server/src/server.ts index 57d99d8..ed085f9 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -106,12 +106,18 @@ export class ModelicaServer { const diagnostics = this.analyzer.analyze(document); } + /** + * Provide symbols defined in document. + * + * @param params Unused. + * @returns Symbol information. + */ private onDocumentSymbol(params: LSP.DocumentSymbolParams): LSP.SymbolInformation[] { // TODO: ideally this should return LSP.DocumentSymbol[] instead of LSP.SymbolInformation[] // which is a hierarchy of symbols. // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentSymbol logger.debug(`onDocumentSymbol`); - return this.analyzer.getDeclarationsForUri({ uri: params.textDocument.uri }); + return this.analyzer.getDeclarationsForUri(params.textDocument.uri); } } diff --git a/server/src/util/declarations.ts b/server/src/util/declarations.ts index d9fa0d8..06b1f2c 100644 --- a/server/src/util/declarations.ts +++ b/server/src/util/declarations.ts @@ -12,12 +12,7 @@ import * as Parser from 'web-tree-sitter'; import * as TreeSitterUtil from './tree-sitter'; import { logger } from './logger'; -const TREE_SITTER_TYPE_TO_LSP_KIND: { [type: string]: LSP.SymbolKind | undefined } = { - // These keys are using underscores as that's the naming convention in tree-sitter. - environment_variable_assignment: LSP.SymbolKind.Variable, - function_definition: LSP.SymbolKind.Function, - variable_assignment: LSP.SymbolKind.Variable, -}; +const isEmpty = (data: string): boolean => typeof data === "string" && data.trim().length == 0; export type GlobalDeclarations = { [word: string]: LSP.SymbolInformation } export type Declarations = { [word: string]: LSP.SymbolInformation[] } @@ -27,58 +22,18 @@ const GLOBAL_DECLARATION_LEAF_NODE_TYPES = new Set([ 'function_definition', ]); -/** - * Returns declarations (functions or variables) from a given root node - * that would be available after sourcing the file. This currently does - * not include global variables defined inside if statements or functions - * as we do not do any flow tracing. - * - * Will only return one declaration per symbol name – the latest definition. - * This behavior is consistent with how Bash behaves, but differs between - * LSP servers. - * - * Used when finding declarations for sourced files and to get declarations - * for the entire workspace. - */ -export function getGlobalDeclarations({ - tree, - uri, -}: { - tree: Parser.Tree - uri: string -}): GlobalDeclarations { - const globalDeclarations: GlobalDeclarations = {}; - - TreeSitterUtil.forEach(tree.rootNode, (node) => { - const followChildren = !GLOBAL_DECLARATION_LEAF_NODE_TYPES.has(node.type); - - const symbol = getDeclarationSymbolFromNode({ node, uri }); - if (symbol) { - const word = symbol.name; - globalDeclarations[word] = symbol; - } - - return followChildren; - }); - - return globalDeclarations; -} - /** * Returns all declarations (functions or variables) from a given tree. - * This includes local variables. + * + * @param tree Tree-sitter tree. + * @param uri The document's uri. + * @returns Symbol information for all declarations. */ -export function getAllDeclarationsInTree({ - tree, - uri, -}: { - tree: Parser.Tree - uri: string -}): LSP.SymbolInformation[] { +export function getAllDeclarationsInTree(tree: Parser.Tree, uri: string): LSP.SymbolInformation[] { const symbols: LSP.SymbolInformation[] = []; TreeSitterUtil.forEach(tree.rootNode, (node) => { - const symbol = getDeclarationSymbolFromNode({ node, uri }); + const symbol = getDeclarationSymbolFromNode(node, uri); if (symbol) { symbols.push(symbol); } @@ -88,136 +43,32 @@ export function getAllDeclarationsInTree({ } /** - * Returns declarations available for the given file and location. - * The heuristics used is a simplification compared to bash behaviour, - * but deemed good enough, compared to the complexity of flow tracing. + * Converts node to symbol information. * - * Used when getting declarations for the current scope. + * @param tree Tree-sitter tree. + * @param uri The document's uri. + * @returns Symbol information from node. */ -export function getLocalDeclarations({ - node, - rootNode, - uri, -}: { - node: Parser.SyntaxNode | null - rootNode: Parser.SyntaxNode - uri: string -}): Declarations { - const declarations: Declarations = {}; - - // Bottom up traversal to capture all local and scoped declarations - const walk = (node: Parser.SyntaxNode | null) => { - // NOTE: there is also node.walk - if (node) { - for (const childNode of node.children) { - let symbol: LSP.SymbolInformation | null = null; - - // local variables - if (childNode.type === 'declaration_command') { - const variableAssignmentNode = childNode.children.filter( - (child) => child.type === 'variable_assignment', - )[0]; - - if (variableAssignmentNode) { - symbol = nodeToSymbolInformation({ - node: variableAssignmentNode, - uri, - }); - } - } else if (childNode.type === 'for_statement') { - const variableNode = childNode.child(1); - if (variableNode && variableNode.type === 'variable_name') { - symbol = LSP.SymbolInformation.create( - variableNode.text, - LSP.SymbolKind.Variable, - TreeSitterUtil.range(variableNode), - uri, - ); - } - } else { - symbol = getDeclarationSymbolFromNode({ node: childNode, uri }); - } - - if (symbol) { - if (!declarations[symbol.name]) { - declarations[symbol.name] = []; - } - declarations[symbol.name].push(symbol); - } - } - - walk(node.parent); - } - }; - - walk(node); - - // Top down traversal to add missing global variables from within functions - Object.entries( - getAllGlobalVariableDeclarations({ - rootNode, - uri, - }), - ).map(([name, symbols]) => { - if (!declarations[name]) { - declarations[name] = symbols; - } - }); - - return declarations; -} - -function getAllGlobalVariableDeclarations({ - uri, - rootNode, -}: { - uri: string - rootNode: Parser.SyntaxNode -}) { - const declarations: Declarations = {}; - - TreeSitterUtil.forEach(rootNode, (node) => { - if ( - node.type === 'variable_assignment' && - // exclude local variables - node.parent?.type !== 'declaration_command' - ) { - const symbol = nodeToSymbolInformation({ node, uri }); - if (symbol) { - if (!declarations[symbol.name]) { - declarations[symbol.name] = []; - } - declarations[symbol.name].push(symbol); - } - } - - return; - }); - - return declarations; -} - -function nodeToSymbolInformation({ - node, - uri, -}: { - node: Parser.SyntaxNode - uri: string -}): LSP.SymbolInformation | null { +export function nodeToSymbolInformation(node: Parser.SyntaxNode, uri: string): LSP.SymbolInformation | null { const named = node.firstNamedChild; if (named === null) { return null; } + const name = TreeSitterUtil.getIdentifier(node); + if (name === null || isEmpty(name)) { + return null; + } + + const kind = getKind(node); + const containerName = TreeSitterUtil.findParent(node, (p) => p.type === 'function_definition') ?.firstNamedChild?.text || ''; - const kind = TREE_SITTER_TYPE_TO_LSP_KIND[node.type]; - return LSP.SymbolInformation.create( - named.text, + name, kind || LSP.SymbolKind.Variable, TreeSitterUtil.range(node), uri, @@ -225,34 +76,49 @@ function nodeToSymbolInformation({ ); } -function getDeclarationSymbolFromNode({ - node, - uri, -}: { - node: Parser.SyntaxNode - uri: string -}): LSP.SymbolInformation | null { +/** + * Get declaration from node and convert to symbol information. + * + * @param node Root node of tree. + * @param uri The associated URI for this document. + * @returns LSP symbol information for definition. + */ +function getDeclarationSymbolFromNode(node: Parser.SyntaxNode, uri: string): LSP.SymbolInformation | null { if (TreeSitterUtil.isDefinition(node)) { - //logger.debug('Found definition:'); - //logger.debug(node.toString()); - return nodeToSymbolInformation({ node, uri }); - } else if (node.type === 'command' && node.text.startsWith(': ')) { - // : does argument expansion and retains the side effects. - // A common usage is to define default values of environment variables, e.g. : "${VARIABLE:="default"}". - const variableNode = node.namedChildren - .find((c) => c.type === 'string') - ?.namedChildren.find((c) => c.type === 'expansion') - ?.namedChildren.find((c) => c.type === 'variable_name'); - - if (variableNode) { - return LSP.SymbolInformation.create( - variableNode.text, - LSP.SymbolKind.Variable, - TreeSitterUtil.range(variableNode), - uri, - ); - } + return nodeToSymbolInformation(node, uri); } return null; } + +/** + * Returns symbol kind from class definition node. + * + * @param node Node containing class_definition + * @returns Symbol kind or `undefined`. + */ +function getKind(node: Parser.SyntaxNode): LSP.SymbolKind | undefined { + + const classPrefixes = TreeSitterUtil.getClassPrefixes(node)?.split(/\s+/); + if (classPrefixes === undefined) { + return undefined; + } + + switch (classPrefixes[classPrefixes.length - 1]) { + case 'block': + case 'class': + case 'connector': + case 'model': + return LSP.SymbolKind.Class; + case 'function': + case 'operator': + return LSP.SymbolKind.Function; + case 'package': + case 'record': + return LSP.SymbolKind.Package; + case 'type': + return LSP.SymbolKind.TypeParameter; + default: + return undefined; + } +} diff --git a/server/src/util/tree-sitter.ts b/server/src/util/tree-sitter.ts index 4d794f6..872510a 100644 --- a/server/src/util/tree-sitter.ts +++ b/server/src/util/tree-sitter.ts @@ -9,11 +9,13 @@ import * as LSP from 'vscode-languageserver/node'; import { SyntaxNode } from 'web-tree-sitter'; +import { logger } from './logger'; + /** * Recursively iterate over all nodes in a tree. * - * @param node The node to start iterating from - * @param callback The callback to call for each node. Return false to stop following children. + * @param node The node to start iterating from + * @param callback The callback to call for each node. Return false to stop following children. */ export function forEach(node: SyntaxNode, callback: (n: SyntaxNode) => void | boolean) { const followChildren = callback(node) !== false; @@ -31,20 +33,15 @@ export function range(n: SyntaxNode): LSP.Range { ); } +/** + * Tell if a node is a definition. + * + * @param n Node of tree + * @returns `true` if node is a definition, `false` otherwise. + */ export function isDefinition(n: SyntaxNode): boolean { switch (n.type) { case 'class_definition': - ///case 'function_definition': - return true; - default: - return false; - } -} - -export function isReference(n: SyntaxNode): boolean { - switch (n.type) { - case 'variable_name': - case 'command_name': return true; default: return false; @@ -64,3 +61,49 @@ export function findParent( } return null; } + +/** + * Get identifier from `class_definition` node. + * + * @param n Syntax tree node. + */ +export function getIdentifier(start: SyntaxNode): string | null { + + let found: boolean = false; + let identifier: string; + + forEach(start, (n) => { + if (n.type == 'IDENT') { + identifier = n.text; + found = true; + return false; + } + return true; + }); + + if (found) { + return identifier!; + } else { + return null; + } +} + +/** + * Get class prefixes from `class_definition` node. + * + * @param node Class definition node. + * @returns String with class prefixes or `null` if no `class_prefixes` can be found. + */ +export function getClassPrefixes(node: SyntaxNode): string | null { + + if (node.type !== 'class_definition') { + return null; + } + + const classPrefixNode = node.childForFieldName('classPrefixes'); + if (classPrefixNode == null || classPrefixNode.type !== 'class_prefixes') { + return null; + } + + return classPrefixNode.text; +} diff --git a/server/tsconfig.json b/server/tsconfig.json index b3d83c8..5aba7a2 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -19,6 +19,7 @@ "exclude": [ "node_modules", ".vscode-test", - "src/test" + "src/test", + "src/util/test" ] } \ No newline at end of file