Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
]
},
"dependencies": {
"antlr4": "^4.13.2",
"cashc": "file:../cashscript/packages/cashc",
"vsce": "^2.15.0",
"vscode-languageclient": "^7.0.0",
"vscode-languageserver": "^7.0.0",
Expand Down
78 changes: 57 additions & 21 deletions src/CashscriptLinter/CashscriptLinter.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,61 @@
import { CharStream, CommonTokenStream } from 'antlr4';
import { Diagnostic } from 'vscode-languageserver';
import { SafeErrorListener, SafeErrorStrategy } from './ErrorListeners';
import CashScriptLexer from './grammar/CashScriptLexer';
import CashScriptParser from './grammar/CashScriptParser';
import { Diagnostic, DiagnosticSeverity, Range } from 'vscode-languageserver';
import { SafeErrorListener } from './ErrorListeners';
import type { CashScriptError, Point } from 'cashc';

type CashcModule = Pick<typeof import('cashc'), 'compileString'>;
type SourcePoint = Pick<Point, 'line' | 'column'>;

// Preserve import() in CommonJS output so VS Code can load cashc's ESM package.
// Note that VS Code extensions *do* support ESM (since April 2025 / v1.100),
// but Cursor does not support this yet, and we do want to support Cursor.
const importCashc = new Function('return import("cashc")') as () => Promise<CashcModule>;
let cashcModule: Promise<CashcModule>;

async function loadCashc(): Promise<CashcModule> {
cashcModule ??= importCashc();
return cashcModule;
}
export default class CashscriptLinter {
static getDiagnostics(code: string): Diagnostic[] {
const errListener = new SafeErrorListener();

const inputStream = new CharStream(code);
const lexer = new CashScriptLexer(inputStream);
lexer.removeErrorListeners();
lexer.addErrorListener(errListener);

const tokenStream = new CommonTokenStream(lexer);
const parser = new CashScriptParser(tokenStream);
parser._errHandler = new SafeErrorStrategy();
parser.removeErrorListeners();
parser.addErrorListener(errListener);
const parseTree = parser.sourceFile();

return errListener.getErrs();
static async getDiagnostics(code: string): Promise<Diagnostic[]> {
const errorListener = new SafeErrorListener();

try {
const { compileString } = await loadCashc();
compileString(code, { errorListener });
} catch (error) {
if (errorListener.getErrs().length === 0) {
return [diagnosticFromCompilerError(error)];
}
}

return errorListener.getErrs();
}
}

function diagnosticFromCompilerError(error: unknown): Diagnostic {
const originalMessage = error instanceof Error ? error.message : String(error);
const message = withoutLocationSuffix(originalMessage);
const location = (error as Partial<CashScriptError> | null | undefined)?.node?.location;
const messageLocation = originalMessage.match(/\bat Line (\d+), Column (\d+)$/);
const fallbackPoint = messageLocation
? { line: Number(messageLocation[1]), column: Number(messageLocation[2]) }
: { line: 1, column: 0 };

const range: Range = {
start: pointToPosition(location?.start ?? fallbackPoint),
end: pointToPosition(location?.end ?? fallbackPoint),
};

return Diagnostic.create(range, message, DiagnosticSeverity.Error);
}

function pointToPosition(point: SourcePoint): Range['start'] {
return {
line: Math.max(point.line - 1, 0),
character: Math.max(point.column, 0),
};
}

function withoutLocationSuffix(message: string): string {
return message.replace(/\s+at Line \d+, Column \d+$/, '');
}
20 changes: 5 additions & 15 deletions src/CashscriptLinter/ErrorListeners.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,22 @@
import { ErrorListener, DefaultErrorStrategy, Parser, RecognitionException, Recognizer } from 'antlr4';
import { Diagnostic, DiagnosticSeverity, Range } from 'vscode-languageserver';

export class SafeErrorListener extends ErrorListener<any> {
static readonly INSTANCE = new SafeErrorListener();

errs: Diagnostic[] = [];
export class SafeErrorListener {
private errs: Diagnostic[] = [];

getErrs(): Diagnostic[] {
return this.errs;
}

syntaxError<T>(
recognizer: Recognizer<T>,
offendingSymbol: T,
_recognizer: unknown,
_offendingSymbol: T,
line: number,
charPositionInLine: number,
message: string,
e?: RecognitionException,
_e?: unknown,
): void {
const capitalisedMessage = message.charAt(0).toUpperCase() + message.slice(1);

//console.log(capitalisedMessage);
const range: Range = {
start: {
line: line - 1,
Expand All @@ -36,9 +32,3 @@ export class SafeErrorListener extends ErrorListener<any> {
this.errs.push(diag);
}
}

export class SafeErrorStrategy extends DefaultErrorStrategy {
sync(recognizer: Parser): void {
return;
}
}
Loading