From e251c662ba1c8ad51e527b9c00388930aca5f77b Mon Sep 17 00:00:00 2001 From: idillon Date: Tue, 19 Dec 2023 19:28:32 -0500 Subject: [PATCH] Feat: Add hover for Yocto defined variables in embedded Python --- server/src/BitBakeDocScanner.ts | 27 +++++ server/src/__tests__/fixtures/hover.bb | 18 ++- server/src/__tests__/hover.test.ts | 139 +++++++++++++++++++++++ server/src/connectionHandlers/onHover.ts | 2 +- server/src/tree-sitter/analyzer.ts | 45 ++++++++ 5 files changed, 229 insertions(+), 2 deletions(-) diff --git a/server/src/BitBakeDocScanner.ts b/server/src/BitBakeDocScanner.ts index 5392f6cf..a580c9d3 100644 --- a/server/src/BitBakeDocScanner.ts +++ b/server/src/BitBakeDocScanner.ts @@ -74,6 +74,7 @@ export class BitBakeDocScanner { private _yoctoVariableInfo: VariableInfo[] = [] private _variableFlagInfo: VariableFlagInfo[] = [] private _yoctoTaskInfo: DocInfo[] = [] + private _pythonDatastoreFunction: string[] = [] private _docPath: string = path.join(__dirname, '../../client/resources/docs') // This default path is for the test. The path after the compilation can be different private readonly _keywordInfo: DocInfo[] = KEYWORDS @@ -97,11 +98,16 @@ export class BitBakeDocScanner { return this._keywordInfo } + get pythonDatastoreFunction (): string[] { + return this._pythonDatastoreFunction + } + public clearScannedDocs (): void { this._bitbakeVariableInfo = [] this._yoctoVariableInfo = [] this._variableFlagInfo = [] this._yoctoTaskInfo = [] + this._pythonDatastoreFunction = [] } public setDocPathAndParse (extensionPath: string): void { @@ -110,6 +116,7 @@ export class BitBakeDocScanner { this.parseBitbakeVariablesFile() this.parseYoctoVariablesFile() this.parseYoctoTaskFile() + this.parsePythonDatastoreFunction() } // TODO: Generalize these parse functions. They all read a file, match some content and store it. @@ -267,6 +274,26 @@ export class BitBakeDocScanner { } this._variableFlagInfo = variableFlagInfo } + + public parsePythonDatastoreFunction (): void { + const filePath = path.join(this._docPath, 'bitbake-user-manual-metadata.rst') + const pattern = /^ {3}\* - ``d\.(?.*)\("X"(.*)\)``/gm + let file = '' + try { + file = fs.readFileSync(filePath, 'utf8') + } catch { + logger.warn(`Failed to read file at ${filePath}`) + } + const pythonDatastoreFunction: string[] = [] + for (const match of file.matchAll(pattern)) { + const name = match.groups?.name + if (name === undefined) { + return + } + pythonDatastoreFunction.push(name) + } + this._pythonDatastoreFunction = pythonDatastoreFunction + } } export const bitBakeDocScanner = new BitBakeDocScanner() diff --git a/server/src/__tests__/fixtures/hover.bb b/server/src/__tests__/fixtures/hover.bb index cc126527..88e481e8 100644 --- a/server/src/__tests__/fixtures/hover.bb +++ b/server/src/__tests__/fixtures/hover.bb @@ -30,4 +30,20 @@ my_do_build(){ inherit dummy include dummy.inc -require dummy.inc \ No newline at end of file +require dummy.inc + +python (){ + d.getVar("DESCRIPTION") + d.setVar('DESCRIPTION', 'value') + b.getVar('DESCRIPTION') + d.test('DESCRIPTION') + d.getVar("FOO") + e.data.getVar('DESCRIPTION') +} + +def test (): + d.setVar('DESCRIPTION') + +VAR = "${@d.getVar("DESCRIPTION")}" + +d.getVar("DESCRIPTION") diff --git a/server/src/__tests__/hover.test.ts b/server/src/__tests__/hover.test.ts index 8af61f65..41da0230 100644 --- a/server/src/__tests__/hover.test.ts +++ b/server/src/__tests__/hover.test.ts @@ -315,4 +315,143 @@ describe('on hover', () => { }) ) }) + + it('shows definition on hovering variable in Python functions for accessing datastore', async () => { + bitBakeDocScanner.parseBitbakeVariablesFile() + bitBakeDocScanner.parsePythonDatastoreFunction() + await analyzer.analyze({ + uri: DUMMY_URI, + document: FIXTURE_DOCUMENT.HOVER + }) + + const shouldShow1 = await onHoverHandler({ + textDocument: { + uri: DUMMY_URI + }, + position: { + line: 35, + character: 14 + } + }) + + const shouldShow2 = await onHoverHandler({ + textDocument: { + uri: DUMMY_URI + }, + position: { + line: 36, + character: 14 + } + }) + + const shouldShow3 = await onHoverHandler({ + textDocument: { + uri: DUMMY_URI + }, + position: { + line: 40, + character: 19 + } + }) + + const shouldShow4 = await onHoverHandler({ + textDocument: { + uri: DUMMY_URI + }, + position: { + line: 46, + character: 20 + } + }) + + const shouldShow5 = await onHoverHandler({ + textDocument: { + uri: DUMMY_URI + }, + position: { + line: 44, + character: 14 + } + }) + + const shouldNotShow1 = await onHoverHandler({ + textDocument: { + uri: DUMMY_URI + }, + position: { + line: 37, + character: 14 + } + }) + + const shouldNotShow2 = await onHoverHandler({ + textDocument: { + uri: DUMMY_URI + }, + position: { + line: 38, + character: 12 + } + }) + + const shouldNotShow3 = await onHoverHandler({ + textDocument: { + uri: DUMMY_URI + }, + position: { + line: 39, + character: 19 + } + }) + + const shouldNotShow4 = await onHoverHandler({ + textDocument: { + uri: DUMMY_URI + }, + position: { + line: 48, + character: 10 + } + }) + + expect(shouldShow1).toEqual({ + contents: { + kind: 'markdown', + value: '**DESCRIPTION**\n___\n A long description for the recipe.\n\n' + } + }) + + expect(shouldShow2).toEqual({ + contents: { + kind: 'markdown', + value: '**DESCRIPTION**\n___\n A long description for the recipe.\n\n' + } + }) + + expect(shouldShow3).toEqual({ + contents: { + kind: 'markdown', + value: '**DESCRIPTION**\n___\n A long description for the recipe.\n\n' + } + }) + + expect(shouldShow4).toEqual({ + contents: { + kind: 'markdown', + value: '**DESCRIPTION**\n___\n A long description for the recipe.\n\n' + } + }) + + expect(shouldShow5).toEqual({ + contents: { + kind: 'markdown', + value: '**DESCRIPTION**\n___\n A long description for the recipe.\n\n' + } + }) + + expect(shouldNotShow1).toBe(null) + expect(shouldNotShow2).toBe(null) + expect(shouldNotShow3).toBe(null) + expect(shouldNotShow4).toBe(null) + }) }) diff --git a/server/src/connectionHandlers/onHover.ts b/server/src/connectionHandlers/onHover.ts index 480c5e0f..a522c330 100644 --- a/server/src/connectionHandlers/onHover.ts +++ b/server/src/connectionHandlers/onHover.ts @@ -18,7 +18,7 @@ export async function onHoverHandler (params: HoverParams): Promise symbol.name === word) && analyzer.isIdentifierOfVariableAssignment(params)) || analyzer.isVariableExpansion(textDocument.uri, position.line, position.character) + const canShowHoverDefinitionForVariableName: boolean = (analyzer.getGlobalDeclarationSymbols(textDocument.uri).some((symbol) => symbol.name === word) && analyzer.isIdentifierOfVariableAssignment(params)) || analyzer.isVariableExpansion(textDocument.uri, position.line, position.character) || analyzer.isPythonDatastoreVariable(textDocument.uri, position.line, position.character) if (canShowHoverDefinitionForVariableName) { const found = [ ...bitBakeDocScanner.bitbakeVariableInfo.filter((bitbakeVariable) => !bitBakeDocScanner.yoctoVariableInfo.some(yoctoVariable => yoctoVariable.name === bitbakeVariable.name)), diff --git a/server/src/tree-sitter/analyzer.ts b/server/src/tree-sitter/analyzer.ts index 8df41df5..6a1a0976 100644 --- a/server/src/tree-sitter/analyzer.ts +++ b/server/src/tree-sitter/analyzer.ts @@ -27,6 +27,7 @@ import { logger } from '../lib/src/utils/OutputLogger' import fs from 'fs' import path from 'path' import { bitBakeProjectScannerClient } from '../BitbakeProjectScannerClient' +import { bitBakeDocScanner } from '../BitBakeDocScanner' const DEBOUNCE_TIME_MS = 500 interface AnalyzedDocument { @@ -274,6 +275,50 @@ export default class Analyzer { (n?.type === ':' && n?.parent?.type === 'ERROR' && n?.parent?.previousSibling?.type === 'override') // when having MYVAR:append: = '123' and the second or later colon is typed } + public isInsidePythonRegion ( + uri: string, + line: number, + column: number + ): boolean { + let n = this.nodeAtPoint(uri, line, column) + + while (n?.parent !== null && n?.parent !== undefined) { + if (TreeSitterUtils.isInlinePython(n.parent) || TreeSitterUtils.isPythonDefinition(n.parent)) { + return true + } + n = n.parent + } + + return false + } + + public isPythonDatastoreVariable ( + uri: string, + line: number, + column: number + ): boolean { + const n = this.nodeAtPoint(uri, line, column) + if (!this.isInsidePythonRegion(uri, line, column)) { + return false + } + // Example: + // n.text: FOO + // n.parent.text: 'FOO' + // n.parent.parent.text: ('FOO') + // n.parent.parent.parent.text: d.getVar('FOO') + const parentParentParent = n?.parent?.parent?.parent + if (parentParentParent?.type !== 'call') { + return false + } + const match = parentParentParent.text.match(/^(d|e\.data)\.(?.*)\((?.*)\)$/) // d.name(params), e.data.name(params) + const functionName = match?.groups?.name + if (functionName === undefined || !(bitBakeDocScanner.pythonDatastoreFunction.includes(functionName))) { + return false + } + const variable = match?.groups?.params?.split(',')[0]?.trim().replace(/('|")/g, '') + return variable === n?.text + } + /** * Check if the variable expansion syntax is being typed. Only for expressions that reference variables. \ * Example: