All files / lib/env error.ts

62.5% Statements 90/144
22.73% Branches 5/22
59.38% Functions 19/32
60.4% Lines 61/101

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183  6x                         6x 5x   5x       6x 5x 5x     6x 5x   6x   6x   6x         6x         6x       6x         6x     6x   6x     6x 4x 4x     6x 4x   6x 6x 6x 6x 6x 6x   6x 3x   3x         6x 1x 1x     6x 1x   6x 6x   6x         6x       6x   6x 1x 1x   1x 1x   1x 1x   1x 1x   1x 1x   1x     6x         6x             6x                                                     6x                     6x   6x     6x      
import { getColumn, getLine, Token } from "lib/env/lexer";
import { NewlineType } from "lib/options";
 
export interface FileCoordinates {
    line: number
    column: number
    position: number // TODO get rid of?
}
 
// Ts-jest doesn't support extending Error, so instead we have to implement it.
// Otherwise, we get this error thrown in our tests when trying to call
//  setters on subclasses of Error: "(intermediate value).setToken is not a function"
//
// This is only an issue with ts-jest, but works fine in the code compiled by esbuild.
export abstract class FileError implements Error {
    public readonly name: string = ''
    
    public readonly message: string = ''
 
    protected filePath: string
 
    public setFilePath(filePath: string): this {
        this.filePath = filePath
        return this
    }
 
    public getFilePath(): string {
        return this.filePath
    }
}
 
export class FileNotFoundError extends FileError {}
 
export class LexicalError extends FileError {
    protected char: string
 
    protected coordinates: FileCoordinates
 
    public setChar(char: string): this {
        this.char = char
        return this
    }
 
    public getChar(): string {
        return this.char
    }
 
    public setCoordinates(coordinates: FileCoordinates): this {
        this.coordinates = coordinates
        return this
    }
 
    public getCoordinates(): FileCoordinates {
        return this.coordinates
    }
}
 
export abstract class ParseError extends FileError {
    protected token: Token
 
    public setToken(token: Token): this {
        this.token = token
        return this
    }
 
    public getToken(): Token {
        return this.token
    }
}
export class InvalidTokenAfterCommentError extends ParseError {}
export class ExpectedAssignmentAfterIdentifierError extends ParseError {}
export class UnexpectedTokenError extends ParseError {}
export class InvalidTokenAfterIdentifierError extends ParseError {}
export class DuplicateVariableError extends ParseError {}
 
export class InvalidArgumentError implements Error {
    public readonly name: string = ''
 
    public readonly message: string = ''
 
    // TODO remove this and use "name"
    protected argumentName: string
 
    public setArgumentName(argumentName: string): this {
        this.argumentName = argumentName
        return this
    }
 
    public getArgumentName(): string {
        return this.argumentName
    }
}
export class InvalidNewlineTypeError extends InvalidArgumentError {}
 
export class MissingArgumentValueError implements Error {
    public name: string = ''
 
    public readonly message: string = ''
 
    public setName (name: string): this {
        this.name = name
        return this
    }
}
 
export const getMessageForError = (error: Error): string => {
    const fileNotFound = error instanceof FileNotFoundError
    Iif (fileNotFound) return getMessageForFileNotFoundError(error as FileNotFoundError)
 
    const isLexicalError = error instanceof LexicalError
    Iif (isLexicalError) return getMessageForLexicalError(error as LexicalError)
 
    const isParseError = error instanceof ParseError
    Iif (isParseError) return getMessageForParseError(error as ParseError)
    
    const isInvalidArgumentError = error instanceof InvalidArgumentError
    Iif (isInvalidArgumentError) return getMessageForInvalidArgumentError(error as InvalidArgumentError)
 
    const isMissingArgumentValueError = error instanceof MissingArgumentValueError
    Iif (isMissingArgumentValueError) return getMessageForMissingArgumentValueError(error as MissingArgumentValueError)
 
    return `ERROR: ${(error).message}`
}
 
const getMessageForFileNotFoundError = (error: FileNotFoundError): string => {
    const filePath = error.getFilePath()
    return `Could not locate ${filePath}`
}
 
const getMessageForLexicalError = (error: LexicalError): string => {
    const filePath = error.getFilePath()
    const coordinates = error.getCoordinates()
    const positionDescription = getPositionDescription(filePath, coordinates)
    return `Unrecognized token ${positionDescription}`
}
 
const getMessageForParseError = (error: ParseError): string => {
    const token = error.getToken()
    const filePath = error.getFilePath()
    const positionDescription = getPositionDescription(filePath, token)
 
    const invalidTokenAfterComment = error instanceof InvalidTokenAfterCommentError
    if (invalidTokenAfterComment) return `Expected newline or end of document after comment ${positionDescription}`
 
    const expectedAssignmentAfterIdentifier = error instanceof ExpectedAssignmentAfterIdentifierError
    if (expectedAssignmentAfterIdentifier) return `Expected = after variable "${token.value}" ${positionDescription}`
 
    const unexpectedToken = error instanceof UnexpectedTokenError
    if (unexpectedToken) return `Unexpected ${token.value} ${positionDescription}`
 
    const invalidTokenAfterIdentifier = error instanceof InvalidTokenAfterIdentifierError
    if (invalidTokenAfterIdentifier) return `Expected line break or comment after variable declaration ${positionDescription}`
 
    const duplicateVariable = error instanceof DuplicateVariableError
    if (duplicateVariable) {
        // TODO put this var in other IFs?
        const positionDescription = getPositionDescription(filePath, token)
        return `Duplicate variable declaration "${token.value}" ${positionDescription}`
    }
 
    return `Unknown parse error ${positionDescription}`
}
 
const getMessageForInvalidArgumentError = (error: InvalidArgumentError) => {
    const isInvalidNewlineTypeError = error instanceof InvalidNewlineTypeError
    if (isInvalidNewlineTypeError) {
        const validTypes = Object.values(NewlineType)
        return `Invalid newline type. Valid types: "${validTypes.join('", "')}"`
    }
    
    const name = error.getArgumentName()
    return `Invalid argument ${name}`
}
 
const sanitizeArgumentName = (name: string): string => name.split('').filter(char => /^[-a-zA-Z]$/.test(char)).join('')
 
const getMessageForMissingArgumentValueError = ({ name }: MissingArgumentValueError): string =>
    `Missing value for argument ${sanitizeArgumentName(name)}`
 
const getPositionDescription = (filePath: string, { line, column }: FileCoordinates): string =>
    `at ${filePath}:${line}:${column}`