Add support for verifying diagnostic messages

This commit is contained in:
Sam Vervaeck 2023-07-01 01:26:18 +02:00
parent 3cfb816d6a
commit bd4ed57c46
Signed by: samvv
SSH key fingerprint: SHA256:dIg0ywU1OP+ZYifrYxy8c5esO72cIKB+4/9wkZj1VaY
5 changed files with 120 additions and 23 deletions

View file

@ -15,7 +15,7 @@ import BoltToJS from "../passes/BoltToJS"
import { stripExtension } from "../util"
import { sync as which } from "which"
import { spawnSync } from "child_process"
import { ConsoleDiagnostics, DiagnosticStore, TypeMismatchDiagnostic } from "../diagnostics"
import { ConsoleDiagnostics, DiagnosticKind, DiagnosticStore, TypeMismatchDiagnostic } from "../diagnostics"
import { Syntax, SyntaxKind, TextFile, isExpression, visitEachChild } from "../cst"
import { Analyser, Checker, parseSourceFile } from ".."
import { typesEqual } from "../types"
@ -163,15 +163,24 @@ program.command('verify', { hidden: true })
const realPath = path.resolve(fileName);
const text = fs.readFileSync(realPath, 'utf-8');
const file = new TextFile(fileName, text);
const sourceFile = parseSourceFile(file, diagnostics);
if (!sourceFile) {
process.exit(1);
}
const analyser = new Analyser();
analyser.addSourceFile(sourceFile);
const checker = new Checker(analyser, diagnostics);
checker.check(sourceFile);
const realDiagnostics = new ConsoleDiagnostics();
let annotationTotalCount = 0;
let annotationErrorCount = 0;
let diagnosticTotalCount = diagnostics.size;
let diagnosticErrorCount = 0;
const visit = (node: Syntax) => {
if (isExpression(node)) {
for (const annotation of node.annotations) {
@ -180,13 +189,37 @@ program.command('verify', { hidden: true })
const expected = checker.getTypeOfNode(annotation.typeExpr);
if (!typesEqual(actual, expected)) {
realDiagnostics.add(new TypeMismatchDiagnostic(actual, expected, [ node ], []));
annotationErrorCount++;
}
annotationTotalCount++;
}
}
}
visitEachChild(node, visit);
}
visit(sourceFile);
const uncaughtDiagnostics = new Set(diagnostics);
for (const [line, comment] of file.comments) {
if (comment[0].kind === SyntaxKind.At && comment[1].kind === SyntaxKind.Identifier && comment[1].text === 'expect_diagnostic' && comment[2].kind === SyntaxKind.StringLiteral) {
for (const diagnostic of uncaughtDiagnostics) {
if (diagnostic.position && diagnostic.position.line === line+1 && DiagnosticKind[diagnostic.kind] === comment[2].contents) {
uncaughtDiagnostics.delete(diagnostic);
}
}
}
}
for (const diagnostic of uncaughtDiagnostics) {
realDiagnostics.add(diagnostic);
}
console.log(`${annotationTotalCount} type annotation(s) verified, ${annotationErrorCount} error(s).`);
console.log(`${diagnosticTotalCount} diagnostic(s) generated, ${uncaughtDiagnostics.size} unexpected.`);
if (realDiagnostics.hasError) {
process.exit(1);
}
});
program.parse();

View file

@ -66,6 +66,8 @@ export class TextRange {
export class TextFile {
public comments = new Map<number, Token[]>();
public constructor(
public origPath: string,
public text: string,

View file

@ -36,7 +36,7 @@ const enum Level {
Fatal,
}
const enum DiagnosticKind {
export enum DiagnosticKind {
UnexpectedChar,
UnexpectedToken,
KindMismatch,
@ -56,6 +56,8 @@ abstract class DiagnosticBase {
public abstract level: Level;
public abstract position: TextPosition | undefined;
}
export class UnexpectedCharDiagnostic extends DiagnosticBase {
@ -89,12 +91,16 @@ export class UnexpectedTokenDiagnostic extends DiagnosticBase {
super();
}
public get position(): TextPosition {
return this.actual.getStartPosition();
}
}
export class TypeclassDeclaredTwiceDiagnostic extends DiagnosticBase {
public readonly kind = DiagnosticKind.TypeclassDecaredTwice;
public level = Level.Error;
public constructor(
@ -102,7 +108,11 @@ export class TypeclassDeclaredTwiceDiagnostic extends DiagnosticBase {
public origDecl: ClassDeclaration,
) {
super();
}
}
public get position(): TextPosition {
return this.name.getStartPosition();
}
}
@ -120,6 +130,10 @@ export class TypeclassNotFoundDiagnostic extends DiagnosticBase {
super();
}
public get position(): TextPosition | undefined {
return this.node?.getFirstToken().getStartPosition();
}
}
export class TypeclassNotImplementedDiagnostic extends DiagnosticBase {
@ -136,6 +150,10 @@ export class TypeclassNotImplementedDiagnostic extends DiagnosticBase {
super();
}
public get position(): TextPosition | undefined {
return this.node?.getFirstToken().getStartPosition();
}
}
export class BindingNotFoundDiagnostic extends DiagnosticBase {
@ -152,6 +170,10 @@ export class BindingNotFoundDiagnostic extends DiagnosticBase {
super();
}
public get position(): TextPosition {
return this.node.getFirstToken().getStartPosition();
}
}
export class TypeMismatchDiagnostic extends DiagnosticBase {
@ -169,6 +191,10 @@ export class TypeMismatchDiagnostic extends DiagnosticBase {
super();
}
public get position(): TextPosition | undefined {
return this.trace[0]?.getFirstToken().getStartPosition();
}
}
export class TupleIndexOutOfRangeDiagnostic extends DiagnosticBase {
@ -184,6 +210,10 @@ export class TupleIndexOutOfRangeDiagnostic extends DiagnosticBase {
super();
}
public get position(): TextPosition | undefined {
return undefined;
}
}
export class FieldNotFoundDiagnostic extends DiagnosticBase {
@ -201,6 +231,10 @@ export class FieldNotFoundDiagnostic extends DiagnosticBase {
super();
}
public get position(): TextPosition | undefined {
return this.cause?.getFirstToken().getStartPosition();
}
}
export class KindMismatchDiagnostic extends DiagnosticBase {
@ -217,6 +251,10 @@ export class KindMismatchDiagnostic extends DiagnosticBase {
super();
}
public get position(): TextPosition | undefined {
return this.origin?.getFirstToken().getStartPosition();
}
}
export class ModuleNotFoundDiagnostic extends DiagnosticBase {
@ -232,6 +270,10 @@ export class ModuleNotFoundDiagnostic extends DiagnosticBase {
super();
}
public get position(): TextPosition | undefined {
return this.node.getFirstToken().getStartPosition();
}
}
export type Diagnostic
@ -256,10 +298,14 @@ export interface Diagnostics {
export class DiagnosticStore implements Diagnostics {
private storage: Diagnostic[] = [];
public hasError = false;
public hasFatal = false;
public get size(): number {
return this.storage.length;
}
public add(diagnostic: Diagnostic): void {
this.storage.push(diagnostic);
if (diagnostic.level >= Level.Error) {
@ -270,8 +316,8 @@ export class DiagnosticStore implements Diagnostics {
}
}
public getDiagnostics(): Iterable<Diagnostic> {
return this.storage;
public [Symbol.iterator](): IterableIterator<Diagnostic> {
return this.storage[Symbol.iterator]();
}
}

View file

@ -4,7 +4,7 @@ import { ParseError, Parser } from "./parser";
import { Punctuator, ScanError, Scanner } from "./scanner";
export function parseSourceFile(file: TextFile, diagnostics: Diagnostics): SourceFile | null {
const scanner = new Scanner(file.text, 0, diagnostics, file);
const scanner = new Scanner(file.text, diagnostics, file);
const punctuated = new Punctuator(scanner);
const parser = new Parser(file, punctuated);
let sourceFile;

View file

@ -97,14 +97,13 @@ export class ScanError extends Error {
export class Scanner extends BufferedStream<Token> {
private currLine = 1;
private currColumn = 1;
private textOffset = 0;
public constructor(
public text: string,
public textOffset: number = 0,
public diagnostics: Diagnostics,
private file: TextFile,
public currPos: TextPosition = new TextPosition(0, 1, 1),
) {
super();
}
@ -115,14 +114,18 @@ export class Scanner extends BufferedStream<Token> {
}
private getChar(): string {
const ch = this.textOffset < this.text.length
? this.text[this.textOffset++]
: EOF
if (ch === '\n') {
this.currLine++;
this.currColumn = 1;
let ch;
if (this.textOffset < this.text.length) {
ch = this.text[this.textOffset++];
this.currPos.offset++;
} else {
this.currColumn++;
ch = EOF;
}
if (ch === '\n') {
this.currPos.line++;
this.currPos.column = 1;
} else {
this.currPos.column++;
}
return ch;
}
@ -141,11 +144,7 @@ export class Scanner extends BufferedStream<Token> {
}
private getCurrentPosition(): TextPosition {
return new TextPosition(
this.textOffset,
this.currLine,
this.currColumn
);
return this.currPos.clone();
}
public read(): Token {
@ -162,13 +161,18 @@ export class Scanner extends BufferedStream<Token> {
continue;
}
if (c0 === '#') {
const line = this.currPos.line;
this.getChar();
let text = '';
for (;;) {
const c1 = this.getChar();
if (c1 === '\n' || c1 === EOF) {
break;
}
text += c1;
}
const scanner = new Scanner(text, this.diagnostics, this.file, this.getCurrentPosition());
this.file.comments.set(line, scanner.getAll());
continue;
}
@ -406,6 +410,18 @@ export class Scanner extends BufferedStream<Token> {
}
public getAll(): Token[] {
const tokens = [];
for (;;) {
const t0 = this.get();
if (t0.kind === SyntaxKind.EndOfFile) {
break;
}
tokens.push(t0);
}
return tokens;
}
}
const enum FrameType {