diff --git a/src/CompileErrorProcessor.ts b/src/CompileErrorProcessor.ts index 0c4b23db..0cde146b 100644 --- a/src/CompileErrorProcessor.ts +++ b/src/CompileErrorProcessor.ts @@ -14,6 +14,12 @@ export class CompileErrorProcessor { private emitter = new EventEmitter(); public compileErrorTimer: NodeJS.Timeout; + private diagnostics: BSDebugDiagnostic[] = []; + + public addDiagnostic(diagnostic: BSDebugDiagnostic) { + this.diagnostics.push(diagnostic); + } + public on(eventName: 'diagnostics', handler: (params: BSDebugDiagnostic[]) => void); public on(eventName: string, handler: (payload: any) => void) { this.emitter.on(eventName, handler); @@ -88,7 +94,7 @@ export class CompileErrorProcessor { } public getErrors(lines: string[]) { - const result: BSDebugDiagnostic[] = []; + let result: BSDebugDiagnostic[] = []; //clone the lines so the parsers can manipulate them lines = [...lines]; while (lines.length > 0) { @@ -111,10 +117,13 @@ export class CompileErrorProcessor { lines.shift(); } } - return result.filter(x => { + result = result.filter(x => { //throw out $livecompile errors (those are generated by REPL/eval code) return x.path && !x.path.toLowerCase().includes('$livecompile'); }); + //include any diagnostics added from external sources (i.e. debug protocol) + result.push(...this.diagnostics); + return result; } /** @@ -147,7 +156,7 @@ export class CompileErrorProcessor { * Parse the standard syntax and compile error format */ private parseSyntaxAndCompileErrors(line: string): BSDebugDiagnostic[] { - let [, message, errorType, code, trailingInfo] = this.execAndTrim( + let [, message, , code, trailingInfo] = this.execAndTrim( // https://regex101.com/r/HHZ6dE/3 /(.*?)(?:\(((?:syntax|compile)\s+error)\s+(&h[\w\d]+)?\s*\))\s*in\b\s+(.+)/ig, line @@ -373,10 +382,9 @@ export interface BSDebugDiagnostic extends Diagnostic { */ path: string; /** - * The name of the component library this diagnostic was emitted from. Should be undefined if diagnostic originated from the - * main app. + * The name of the library (i.e. component library) this diagnostic was emitted from. Should be undefined if diagnostic originated from the main app. */ - componentLibraryName?: string; + libraryName?: string; } export enum CompileStatus { diff --git a/src/adapters/DebugProtocolAdapter.ts b/src/adapters/DebugProtocolAdapter.ts index a4d1c951..80297fd2 100644 --- a/src/adapters/DebugProtocolAdapter.ts +++ b/src/adapters/DebugProtocolAdapter.ts @@ -17,6 +17,7 @@ import type { AdapterOptions, HighLevelType, RokuAdapterEvaluateResponse } from import type { BreakpointManager } from '../managers/BreakpointManager'; import type { ProjectManager } from '../managers/ProjectManager'; import { ActionQueue } from '../managers/ActionQueue'; +import { DiagnosticSeverity, util as bscUtil } from 'brighterscript'; /** * A class that connects to a Roku device over telnet debugger port and provides a standardized way of interacting with it. @@ -246,6 +247,24 @@ export class DebugProtocolAdapter { }); }); + this.socketDebugger.on('compile-error', (response) => { + this.compileErrorProcessor.addDiagnostic({ + message: response.message, + range: bscUtil.createRange( + //convert 1-based debug-protocol line to 0-based Range + response.lineNumber - 1, + 0, + //convert 1-based debug-protocol line to 0-based Range + response.lineNumber - 1, + 999 + ), + //all 'compile-error' events are actual errors + severity: DiagnosticSeverity.Error, + path: response.filePath, + libraryName: response.libraryName + }); + }); + this.socketDebugger.on('cannot-continue', () => { this.emit('cannot-continue'); }); @@ -282,7 +301,7 @@ export class DebugProtocolAdapter { try { this.compileClient = new Socket(); this.compileErrorProcessor.on('diagnostics', (errors) => { - this.compileClient.end(); + this.compileClient?.end(); this.emit('diagnostics', errors); }); diff --git a/src/debugProtocol/Debugger.ts b/src/debugProtocol/Debugger.ts index ddb23345..3d33914c 100644 --- a/src/debugProtocol/Debugger.ts +++ b/src/debugProtocol/Debugger.ts @@ -27,6 +27,7 @@ import { AddBreakpointsResponse } from './responses/AddBreakpointsResponse'; import { RemoveBreakpointsResponse } from './responses/RemoveBreakpointsResponse'; import { util } from '../util'; import { BreakpointErrorUpdateResponse } from './responses/BreakpointErrorUpdateResponse'; +import { CompileErrorUpdateResponse } from './responses/CompileErrorUpdateResponse'; export class Debugger { @@ -120,6 +121,7 @@ export class Debugger { public on(eventName: 'app-exit' | 'cannot-continue' | 'close' | 'start', handler: () => void); public on(eventName: 'data', handler: (data: any) => void); public on(eventName: 'runtime-error' | 'suspend', handler: (data: UpdateThreadsResponse) => void); + public on(eventName: 'compile-error', handler: (data: CompileErrorUpdateResponse) => void); public on(eventName: 'connected', handler: (connected: boolean) => void); public on(eventName: 'io-output', handler: (output: string) => void); public on(eventName: 'protocol-version', handler: (data: ProtocolVersionDetails) => void); @@ -134,6 +136,7 @@ export class Debugger { } private emit(eventName: 'suspend' | 'runtime-error', data: UpdateThreadsResponse); + private emit(eventName: 'compile-error', data: CompileErrorUpdateResponse); private emit(eventName: 'app-exit' | 'cannot-continue' | 'close' | 'connected' | 'data' | 'handshake-verified' | 'io-output' | 'protocol-version' | 'start', data?); private emit(eventName: string, data?) { //emit these events on next tick, otherwise they will be processed immediately which could cause issues @@ -496,7 +499,10 @@ export class Debugger { //we do nothing with breakpoint errors at this time. return this.checkResponse(response, buffer, packetLength); case UPDATE_TYPES.COMPILE_ERROR: - return this.checkResponse(new UndefinedResponse(slicedBuffer), buffer, packetLength); + const compileError = new CompileErrorUpdateResponse(slicedBuffer); + //emit as an event for the adapter to handle + this.emit('compile-error', compileError); + return this.checkResponse(compileError, buffer, packetLength); default: return this.checkResponse(new UndefinedResponse(slicedBuffer), buffer, packetLength); } diff --git a/src/debugProtocol/responses/CompileErrorUpdateResponse.ts b/src/debugProtocol/responses/CompileErrorUpdateResponse.ts new file mode 100644 index 00000000..8d0fad61 --- /dev/null +++ b/src/debugProtocol/responses/CompileErrorUpdateResponse.ts @@ -0,0 +1,68 @@ +import { SmartBuffer } from 'smart-buffer'; +import { util } from '../../util'; +import { UPDATE_TYPES } from '../Constants'; + +/** + * Data sent as the data segment of message type: COMPILE_ERROR + ``` + struct CompileErrorUpdateData { + uint32 flags; // Always 0, reserved for future use + utf8z error_string; + utf8z file_spec; + uint32 line_number; + utf8z library_name; + } + ``` +*/ +export class CompileErrorUpdateResponse { + + constructor(buffer: Buffer) { + // The minimum size of a undefined response + if (buffer.byteLength >= 12) { + let bufferReader = SmartBuffer.fromBuffer(buffer); + this.requestId = bufferReader.readUInt32LE(); + + // Updates will always have an id of zero because we didn't ask for this information + if (this.requestId === 0) { + this.errorCode = bufferReader.readUInt32LE(); + this.updateType = bufferReader.readUInt32LE(); + } + if (this.updateType === UPDATE_TYPES.COMPILE_ERROR) { + try { + this.flags = bufferReader.readUInt32LE(); // flags - always 0, reserved for future use + this.message = util.readStringNT(bufferReader); // error_string + this.filePath = util.readStringNT(bufferReader); // file_spec + this.lineNumber = bufferReader.readUInt32LE(); //line_number + this.libraryName = util.readStringNT(bufferReader); //library_name + + } catch (error) { + // Could not process + } + } + } + } + public success = false; + public readOffset = 0; + public requestId = -1; + public errorCode = -1; + public updateType = -1; + + public flags: number; + + /** + * The message for this compile error + */ + public message: string; + /** + * The file path where the compile error occurred. (in the form `/source/file.brs` or `/components/a/b/c.xml`) + */ + public filePath: string; + /** + * The 1-based line number where the compile error occurred + */ + public lineNumber: number; + /** + * The name of the library where this compile error occurred. (is empty string if for the main app) + */ + public libraryName: string; +} diff --git a/src/debugSession/BrightScriptDebugSession.ts b/src/debugSession/BrightScriptDebugSession.ts index c3b39d39..9d26f1e2 100644 --- a/src/debugSession/BrightScriptDebugSession.ts +++ b/src/debugSession/BrightScriptDebugSession.ts @@ -49,7 +49,6 @@ import type { AugmentedSourceBreakpoint } from '../managers/BreakpointManager'; import { BreakpointManager } from '../managers/BreakpointManager'; import type { LogMessage } from '../logging'; import { logger, debugServerLogOutputEventTransport } from '../logging'; -import { waitForDebugger } from 'inspector'; export class BrightScriptDebugSession extends BaseDebugSession { public constructor() { @@ -372,7 +371,8 @@ export class BrightScriptDebugSession extends BaseDebugSession { * Anytime a roku adapter emits diagnostics, this methid is called to handle it. */ private async handleDiagnostics(diagnostics: BSDebugDiagnostic[]) { - // Roku device and sourcemap work with 1-based line numbers, VSCode expects 0-based lines. + const result = new Map(); + for (let diagnostic of diagnostics) { let sourceLocation = await this.projectManager.getSourceLocation(diagnostic.path, diagnostic.range.start.line + 1); if (sourceLocation) { @@ -383,9 +383,27 @@ export class BrightScriptDebugSession extends BaseDebugSession { // TODO: may need to add a custom event if the source location could not be found by the ProjectManager diagnostic.path = fileUtils.removeLeadingSlash(util.removeFileScheme(diagnostic.path)); } + //override all diagnostic sources to be from `bsdebug` + diagnostic.source = `brs-debug`; + const key = [ + s`${diagnostic.path}`, + diagnostic.range.start.line, + diagnostic.range.start.character, + diagnostic.range.end.line, + diagnostic.range.end.character, + diagnostic.code, + diagnostic.message.trim(), + diagnostic.libraryName + ].join('-'); + //only keep one diagnosic per unique key + result.set(key, diagnostic); } - this.sendEvent(new DiagnosticsEvent(diagnostics)); + this.sendEvent(new DiagnosticsEvent([...result.values()])); + + //small timeout before killing the adapter so that the diagnostics can be properly sent to the client + await util.sleep(500); + //stop the roku adapter and exit the channel void this.rokuAdapter.destroy(); void this.rokuDeploy.pressHomeButton(this.launchConfiguration.host, this.launchConfiguration.remotePort); @@ -1092,7 +1110,7 @@ export class BrightScriptDebugSession extends BaseDebugSession { // If the roku says it can't continue, we are no longer able to debug, so kill the debug session this.rokuAdapter.on('cannot-continue', () => { - this.sendEvent(new TerminatedEvent()); + //this.sendEvent(new TerminatedEvent()); }); //make the connection