From 5096c768fcb9c26731c0bf4e3a84846e2b91365e Mon Sep 17 00:00:00 2001 From: idillon Date: Wed, 4 Oct 2023 22:24:46 -0400 Subject: [PATCH 1/2] Fix: Fix parseAllRecipes - Lines with errors are underlined - Use spawnSync instead of execSync - Improve handling of errors for child_process - Simplify parts of code --- server/src/BitBakeProjectScanner.ts | 205 ++++++++++++---------------- server/src/OutputParser.ts | 76 ++++------- server/src/ProblemsContainer.ts | 66 ++------- server/src/server.ts | 11 +- 4 files changed, 132 insertions(+), 226 deletions(-) diff --git a/server/src/BitBakeProjectScanner.ts b/server/src/BitBakeProjectScanner.ts index dab06ace..0e12b261 100644 --- a/server/src/BitBakeProjectScanner.ts +++ b/server/src/BitBakeProjectScanner.ts @@ -166,32 +166,28 @@ export class BitBakeProjectScanner { private scanAvailableLayers (): void { this._layers = new Array < LayerInfo >() - const output: string = this.executeBitBakeCommand('bitbake-layers show-layers') - - if (output.length > 0) { - try { - let tempStr: string[] = output.split('\n') - tempStr = tempStr.slice(2) - - for (const element of tempStr) { - const tempElement: string[] = element.split(/\s+/) - const layerElement = { - name: tempElement[0], - path: tempElement[1], - priority: parseInt(tempElement[2]) - } - - if ((layerElement.name !== undefined) && (layerElement.path !== undefined) && layerElement.priority !== undefined) { - this._layers.push(layerElement) - } + const commandResult = this.executeBitBakeCommand('bitbake-layers show-layers') + + if (commandResult.status === 0) { + const output = commandResult.stdout.toString() + const outputLines = output.split('\n') + + for (const element of outputLines.slice(2)) { + const tempElement: string[] = element.split(/\s+/) + const layerElement = { + name: tempElement[0], + path: tempElement[1], + priority: parseInt(tempElement[2]) } - } catch (error) { - if (typeof error !== 'string') { - throw error + + if ((layerElement.name !== undefined) && (layerElement.path !== undefined) && layerElement.priority !== undefined) { + this._layers.push(layerElement) } - logger.error(`can not scan available layers error: ${error}`) - this._outputParser.parse(error) } + } else { + const error = commandResult.stderr.toString() + logger.error(`can not scan available layers error: ${error}`) + this._outputParser.parse(error) } } @@ -225,50 +221,39 @@ export class BitBakeProjectScanner { scanForRecipes (): void { this._recipes = new Array < ElementInfo >() - const output: string = this.executeBitBakeCommand('bitbake-layers show-recipes') - - if (output.length > 0) { - const outerReg: RegExp = /(.+):\n((?:\s+\S+\s+\S+(?:\s+\(skipped\))?\n)+)/g - const innerReg: RegExp = /\s+(\S+)\s+(\S+(?:\s+\(skipped\))?)\n/g - let match: RegExpExecArray | null + const commandResult = this.executeBitBakeCommand('bitbake-layers show-recipes') + const output = commandResult.output.toString() - while ((match = outerReg.exec(output)) !== null) { - if (match.index === outerReg.lastIndex) { - outerReg.lastIndex++ - } - - let matchInner: RegExpExecArray | null - const extraInfoString: string[] = new Array < string >() - let layerName: string - let version: string = '' + const outerReg: RegExp = /(.+):\n((?:\s+\S+\s+\S+(?:\s+\(skipped\))?\n)+)/g + const innerReg: RegExp = /\s+(\S+)\s+(\S+(?:\s+\(skipped\))?)\n/g - while ((matchInner = innerReg.exec(match[2])) !== null) { - if (matchInner.index === innerReg.lastIndex) { - innerReg.lastIndex++ - } - - if (extraInfoString.length === 0) { - layerName = matchInner[1] - version = matchInner[2] - } + for (const match of output.matchAll(outerReg)) { + const extraInfoString: string[] = new Array < string >() + let layerName: string + let version: string = '' - extraInfoString.push(`layer: ${matchInner[1]}`) - extraInfoString.push(`version: ${matchInner[2]} `) + for (const matchInner of match[2].matchAll(innerReg)) { + if (extraInfoString.length === 0) { + layerName = matchInner[1] + version = matchInner[2] } - const layer = this._layers.find((obj: LayerInfo): boolean => { - return obj.name === layerName - }) + extraInfoString.push(`layer: ${matchInner[1]}`) + extraInfoString.push(`version: ${matchInner[2]} `) + } - const element: ElementInfo = { - name: match[1], - extraInfo: extraInfoString.join('\n'), - layerInfo: layer, - version - } + const layer = this._layers.find((obj: LayerInfo): boolean => { + return obj.name === layerName + }) - this._recipes.push(element) + const element: ElementInfo = { + name: match[1], + extraInfo: extraInfoString.join('\n'), + layerInfo: layer, + version } + + this._recipes.push(element) } this.scanForRecipesPath() @@ -276,24 +261,17 @@ export class BitBakeProjectScanner { parseAllRecipes (): boolean { logger.debug('parseAllRecipes') - let parsingOutput: string let parsingSuccess: boolean = true - try { - parsingOutput = this.executeBitBakeCommand('bitbake -p') - } catch (error) { - if (typeof error !== 'string') { - throw error - } - logger.error(`parsing all recipes is abborted: ${error}`) - parsingOutput = error - } - - if (parsingOutput.length > 0) { - this._outputParser.parse(parsingOutput) - if (this._outputParser.errorsFound()) { - this._outputParser.reportProblems() - parsingSuccess = false + const commandResult = this.executeBitBakeCommand('bitbake -p') + const output = commandResult.output.toString() + this._outputParser.parse(output) + if (this._outputParser.errorsFound()) { + this._outputParser.reportProblems() + parsingSuccess = false + } else { + if (commandResult.status !== 0) { + logger.warn('Unhandled parsing error:', output) } } return parsingSuccess @@ -322,72 +300,63 @@ export class BitBakeProjectScanner { logger.info(`${recipesWithOutPath.length} recipes must be examined more deeply.`) for (const recipeWithOutPath of recipesWithOutPath) { - const output: string = this.executeBitBakeCommand(`bitbake-layers show-recipes -f ${recipeWithOutPath.name}`) + const commandResult = this.executeBitBakeCommand(`bitbake-layers show-recipes -f ${recipeWithOutPath.name}`) + const output = commandResult.output.toString() const regExp: RegExp = /(\s.*\.bb)/g - let match: RegExpExecArray | null - - if (output.length > 0) { - while ((match = regExp.exec(output)) !== null) { - if (match.index === regExp.lastIndex) { - regExp.lastIndex++ - } - recipeWithOutPath.path = path.parse(match[0].trim()) - } + for (const match of output.matchAll(regExp)) { + recipeWithOutPath.path = path.parse(match[0].trim()) } } } } private scanRecipesAppends (): void { - const output: string = this.executeBitBakeCommand('bitbake-layers show-appends') + const commandResult = this.executeBitBakeCommand('bitbake-layers show-appends') + const output = commandResult.output.toString() - if (output.length > 0) { - const outerReg: RegExp = /(\S.*\.bb):(?:\s*\/\S*.bbappend)+/g + const outerReg: RegExp = /(\S.*\.bb):(?:\s*\/\S*.bbappend)+/g - let match: RegExpExecArray | null + for (const match of output.matchAll(outerReg)) { + const fullRecipeNameAsArray: string[] = match[1].split('_') - while ((match = outerReg.exec(output)) !== null) { - if (match.index === outerReg.lastIndex) { - outerReg.lastIndex++ - } - let matchInner: RegExpExecArray | null - const fullRecipeNameAsArray: string[] = match[1].split('_') - - if (fullRecipeNameAsArray.length > 0) { - const recipeName: string = fullRecipeNameAsArray[0] - - const recipe: ElementInfo | undefined = this.recipes.find((obj: ElementInfo): boolean => { - return obj.name === recipeName - }) + if (fullRecipeNameAsArray.length > 0) { + const recipeName: string = fullRecipeNameAsArray[0] - if (recipe !== undefined) { - const innerReg: RegExp = /(\S*\.bbappend)/g - - while ((matchInner = innerReg.exec(match[0])) !== null) { - if (matchInner.index === innerReg.lastIndex) { - innerReg.lastIndex++ - } + const recipe: ElementInfo | undefined = this.recipes.find((obj: ElementInfo): boolean => { + return obj.name === recipeName + }) - if (recipe.appends === undefined) { - recipe.appends = new Array < PathInfo >() - } + if (recipe !== undefined) { + const innerReg: RegExp = /(\S*\.bbappend)/g - recipe.appends.push(path.parse(matchInner[0])) + for (const matchInner of match[0].matchAll(innerReg)) { + if (recipe.appends === undefined) { + recipe.appends = new Array < PathInfo >() } + + recipe.appends.push(path.parse(matchInner[0])) } } } } } - private executeBitBakeCommand (command: string): string { + private executeBitBakeCommand (command: string): childProcess.SpawnSyncReturns { const scriptContent: string = this.generateBitBakeCommand(command) - return this.executeCommand(scriptContent) + const commandResult = this.executeCommand(scriptContent) + + if (commandResult.status === 127) { + // Likely "bitbake: not found" + // TODO: Show a proper error message to help the user configuring the extension + throw new Error(commandResult.output.toString()) + } + + return commandResult } - private executeCommand (command: string): string { - return childProcess.execSync(command).toString() + private executeCommand (command: string): childProcess.SpawnSyncReturns { + return childProcess.spawnSync(command, { shell: true }) } private generateBitBakeCommand (bitbakeCommand: string): string { @@ -395,7 +364,7 @@ export class BitBakeProjectScanner { if (fs.existsSync(this._pathToEnvScript)) { logger.info('using env script') - scriptFileBuffer.push('source ./' + this._pathToEnvScript + ' ' + this._pathToBuildFolder + ' > /dev/null') + scriptFileBuffer.push(`. ./${this._pathToEnvScript} ${this._pathToBuildFolder} > /dev/null`) } else { logger.info('not using env script') scriptFileBuffer.push( diff --git a/server/src/OutputParser.ts b/server/src/OutputParser.ts index 8e586e7d..3602c19f 100644 --- a/server/src/OutputParser.ts +++ b/server/src/OutputParser.ts @@ -5,11 +5,11 @@ import logger from 'winston' import { + DiagnosticSeverity, type Connection } from 'vscode-languageserver' import { - type ProblemType, ProblemsContainer } from './ProblemsContainer' @@ -22,63 +22,39 @@ export class OutputParser { } parse (message: string): void { - const regex: RegExp = /\s(WARNING:|ERROR:)\s(.*)/g - let m this.clearAllProblemsAndReport() - - while ((m = regex.exec(message)) !== null) { - // This is necessary to avoid infinite loops with zero-width matches - if (m.index === regex.lastIndex) { - regex.lastIndex++ + const severityRegex: RegExp = /\b(WARNING:|ERROR:)/ + let currentSeverity: DiagnosticSeverity = DiagnosticSeverity.Error // dummy initializer which will never be used + let currentMessageLines: string[] = [] + for (const line of message.split(/\r?\n/g).reverse()) { + currentMessageLines.push(line) + const unparsedSeverity = line.match(severityRegex)?.[1] + if (unparsedSeverity === 'ERROR:') { + currentSeverity = DiagnosticSeverity.Error + } else if (unparsedSeverity === 'WARNING:') { + currentSeverity = DiagnosticSeverity.Warning } - - let problemType: ProblemType - if (m[1] === 'ERROR:') { - problemType = 'error' - } else if (m[1] === 'WARNING:') { - problemType = 'warning' - } else { - return + if (unparsedSeverity !== undefined) { // if we reached the first line of a problem + this.addProblem(currentSeverity, currentMessageLines.reverse().join('\n')) + currentMessageLines = [] } - - const tempProblemContainer: ProblemsContainer[] = ProblemsContainer.createProblemContainer(problemType, m[2]) - - tempProblemContainer.forEach((container: ProblemsContainer) => { - const element: ProblemsContainer | undefined = this._problems.find((other: ProblemsContainer) => { - if (other.url === container.url) { - return true - } else { - return false - } - }) - - if (element !== undefined) { - element.problems = element.problems.concat(container.problems) - } else { - this._problems.push(container) - } - }) } } - errorsFound (): boolean { - let errorFound: boolean = false - const BreakException = new Error() - - try { - this._problems.forEach((container: ProblemsContainer) => { - if (container.containsErrors()) { - errorFound = true - throw BreakException - } - }) - } catch (error) { - if (error !== BreakException) { - throw error + private addProblem (severity: DiagnosticSeverity, message: string): void { + const tempProblemContainer: ProblemsContainer[] = ProblemsContainer.createProblemContainer(severity, message) + tempProblemContainer.forEach((container: ProblemsContainer) => { + const element = this._problems.find((other) => other.url === container.url) + if (element !== undefined) { + element.problems = element.problems.concat(container.problems) + } else { + this._problems.push(container) } - } + }) + } - return errorFound + errorsFound (): boolean { + return this._problems.some((container) => container.containsErrors()) } reportProblems (): void { diff --git a/server/src/ProblemsContainer.ts b/server/src/ProblemsContainer.ts index 904a942c..ab5ee452 100644 --- a/server/src/ProblemsContainer.ts +++ b/server/src/ProblemsContainer.ts @@ -9,8 +9,6 @@ import { type PublishDiagnosticsParams } from 'vscode-languageserver' -export type ProblemType = 'error' | 'warning' - export class ProblemsContainer { _url: string = 'file://' _problems: Diagnostic[] = [] @@ -32,23 +30,7 @@ export class ProblemsContainer { } containsErrors (): boolean { - let errorFound: boolean = false - const BreakException = new Error() - - try { - this._problems.forEach((value: Diagnostic) => { - if (value.severity === DiagnosticSeverity.Error) { - errorFound = true - throw BreakException - } - }) - } catch (error) { - if (error !== BreakException) { - throw error - } - } - - return errorFound + return this.problems.some((problem) => problem.severity === DiagnosticSeverity.Error) } getDignosticData (): PublishDiagnosticsParams { @@ -72,30 +54,23 @@ export class ProblemsContainer { objectAsString += `${problem.message},` }) - objectAsString += '}' + objectAsString += ']}' return objectAsString } - static createProblemContainer (type: ProblemType, message: string): ProblemsContainer[] { - const regex = /(ParseError)(?:\s|\w)*\s(\/.*\..*):(\d):\s(.*)/g - let m + static createProblemContainer (severity: DiagnosticSeverity, message: string): ProblemsContainer[] { + const regex = /(ParseError)(?: at )(\/.*):(\d*): (.*)/g const problemContainer: ProblemsContainer[] = [] - - while ((m = regex.exec(message)) !== null) { - // This is necessary to avoid infinite loops with zero-width matches - if (m.index === regex.lastIndex) { - regex.lastIndex++ - } - + for (const match of message.matchAll(regex)) { const problem = new ProblemsContainer() - problem._url = encodeURI('file://' + m[2]) - problem.appendDiagnostic(this.createProblemElement(type, m[4], Number.parseInt(m[3]), m[1])) + problem._url = encodeURI('file://' + match[2]) + problem.appendDiagnostic(this.createProblemElement(severity, match[4], Number.parseInt(match[3]) - 1, match[1])) problemContainer.push(problem) } if (problemContainer.length === 0) { const problem = new ProblemsContainer() - problem.appendDiagnostic(this.createProblemElement(type, message)) + problem.appendDiagnostic(this.createProblemElement(severity, message)) problemContainer.push(problem) } @@ -103,38 +78,25 @@ export class ProblemsContainer { } private static createProblemElement ( - type: ProblemType, + severity: DiagnosticSeverity, message: string, - lineNumber: number = 1, + lineNumber: number = 0, problemCode: string = 'general' ): Diagnostic { - let problemSeverity: DiagnosticSeverity - - if (type === 'error') { - problemSeverity = DiagnosticSeverity.Error - } else if (type === 'warning') { - problemSeverity = DiagnosticSeverity.Warning - } else { - const _exhaustiveCheck: never = type - return _exhaustiveCheck - } - - const problem: Diagnostic = { + return { range: { start: { line: lineNumber, - character: lineNumber + character: 0 }, end: { line: lineNumber, - character: lineNumber + character: Number.MAX_VALUE // whole line } }, - severity: problemSeverity, + severity, message, code: problemCode } - - return problem } } diff --git a/server/src/server.ts b/server/src/server.ts index 4dcec777..ce6e7e89 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -104,12 +104,6 @@ connection.onCompletionResolve((item: CompletionItem): CompletionItem => { return item }) -connection.onDidSaveTextDocument((event) => { - logger.debug(`onDidSaveTextDocument ${JSON.stringify(event)}`) - - bitBakeProjectScanner.parseAllRecipes() -}) - connection.onExecuteCommand((params) => { logger.info(`executeCommand ${JSON.stringify(params)}`) @@ -189,4 +183,9 @@ documents.onDidClose((event) => { setSymbolScanner(null) }) +documents.onDidSave((event) => { + logger.debug(`onDidSave ${JSON.stringify(event)}`) + bitBakeProjectScanner.parseAllRecipes() +}) + documents.listen(connection) From acb9258486b68da1faaee2d0b717527d9945f03e Mon Sep 17 00:00:00 2001 From: idillon Date: Fri, 6 Oct 2023 13:07:07 -0400 Subject: [PATCH 2/2] Chore: Move 'rescanProject' to onDidChangeConfiguration This allow the user to fix their configuration after opening the workspace. This also removes the timeout. --- server/src/server.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/server/src/server.ts b/server/src/server.ts index ce6e7e89..0ef085ac 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -34,10 +34,6 @@ connection.onInitialize((params): InitializeResult => { const workspaceRoot = params.rootPath ?? '' bitBakeProjectScanner.setProjectPath(workspaceRoot) - setTimeout(() => { - bitBakeProjectScanner.rescanProject() - }, 500) - return { capabilities: { textDocumentSync: TextDocumentSyncKind.Incremental, @@ -80,6 +76,8 @@ connection.onDidChangeConfiguration((change) => { bitBakeProjectScanner.pathToBuildFolder = settings.bitbake.pathToBuildFolder bitBakeProjectScanner.pathToBitbakeFolder = settings.bitbake.pathToBitbakeFolder bitBakeDocScanner.parse(settings.bitbake.pathToBitbakeFolder) + + bitBakeProjectScanner.rescanProject() }) connection.onDidChangeWatchedFiles((change) => {