Add support for verifying diagnostic messages
This commit is contained in:
parent
3cfb816d6a
commit
bd4ed57c46
5 changed files with 120 additions and 23 deletions
|
@ -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();
|
||||
|
|
|
@ -66,6 +66,8 @@ export class TextRange {
|
|||
|
||||
export class TextFile {
|
||||
|
||||
public comments = new Map<number, Token[]>();
|
||||
|
||||
public constructor(
|
||||
public origPath: string,
|
||||
public text: string,
|
||||
|
|
|
@ -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]();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue