Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove Interpreter and Testing Refactor #1745

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Prev Previous commit
Next Next commit
Add tests for parser rules
leeyi45 committed Mar 4, 2025
commit ed8eb2ca5bd1809a6631e2ecb7e9893bfe90bc91
10 changes: 4 additions & 6 deletions src/parser/source/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { parse as acornParse, Token, tokenizer } from 'acorn'
import * as es from 'estree'
import type * as es from 'estree'

import { DEFAULT_ECMA_VERSION } from '../../constants'
import { Chapter, Context, Node, Rule, SourceError, Variant } from '../../types'
import { Chapter, Context, Node, SourceError, Variant } from '../../types'
import { ancestor, AncestorWalkerFn } from '../../utils/walkers'
import { DisallowedConstructError, FatalSyntaxError } from '../errors'
import { AcornOptions, Parser } from '../types'
import type { AcornOptions, Rule, Parser } from '../types'
import { createAcornParserOptions, positionToSourceLocation } from '../utils'
import { mapToObj } from '../../utils/misc'
import defaultRules from './rules'
import syntaxBlacklist from './syntax'

@@ -17,9 +18,6 @@ const combineAncestorWalkers =
w2(node, state, ancestors)
}

const mapToObj = <T>(map: Map<string, T>) =>
Array.from(map).reduce((obj, [k, v]) => Object.assign(obj, { [k]: v }), {})

export class SourceParser implements Parser<AcornOptions> {
private chapter: Chapter
private variant: Variant
97 changes: 97 additions & 0 deletions src/parser/source/rules/__tests__/rules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { parseError } from '../../../..'
import { Chapter, Variant } from '../../../../types'
import { parse } from '../../../parser'
import type { Rule } from '../../../types'
import rules from '..'
import { DisallowedConstructError } from '../../../errors'
import { mockContext } from '../../../../utils/testing/mocks'

const chaptersToTest = [
Chapter.SOURCE_1,
Chapter.SOURCE_2,
Chapter.SOURCE_3,
Chapter.SOURCE_4,
Chapter.LIBRARY_PARSER
]

function testSingleChapter(
code: string,
expected: string | undefined,
chapter: Chapter,
variant: Variant = Variant.DEFAULT
) {
const context = mockContext(chapter, variant)
parse(code, context)

// Parser also produces errors for syntax blacklist
// We're not interested in those errors here
const errors = context.errors.filter(err => !(err instanceof DisallowedConstructError))
if (expected === undefined) {
if (errors.length > 0) {
console.error(parseError(errors))
}

expect(errors.length).toEqual(0)
} else {
expect(errors.length).toBeGreaterThanOrEqual(1)
const parsedErrors = parseError(errors)
expect(parsedErrors).toEqual(expect.stringContaining(expected))
}
}

function testMultipleChapters(code: string, expected: string | undefined, rule: Rule<any>) {
const chapterCases = chaptersToTest.map(chapter => {
// If the rule has a `disableFromChapter` set, then for every chapter below it,
// the rule should error, and not error for every chapter above it
const isExpectedToError =
expected !== undefined &&
(rule.disableFromChapter === undefined || chapter < rule.disableFromChapter)
const errStr = isExpectedToError ? 'error' : 'no error'

return [
`Chapter ${chapter}: ${errStr}`,
code,
isExpectedToError ? expected : undefined,
chapter
] as [string, string, string | undefined, Chapter]
})

test.each(chapterCases)('%s', (_, code, expected, chapter) => {
testSingleChapter(code, expected, chapter)
})
}

describe('General rule tests', () => {
rules.forEach(rule => {
if (!rule.testSnippets) {
console.warn(`${rule.name} has no tests`)
return
}

const disableStr = rule.disableFromChapter
? `(Disabled for Chapter ${rule.disableFromChapter} and above)`
: '(Always enabled)'
describe(`Testing ${rule.name} ${disableStr}`, () => {
if (rule.testSnippets!.length === 1) {
const [[code, expected]] = rule.testSnippets!
testMultipleChapters(code, expected, rule)
} else {
rule.testSnippets!.forEach((snippet, i) =>
describe(`Testing Snippet ${i + 1}`, () => testMultipleChapters(...snippet, rule))
)
}
})
})
})

test('no-unspecified-operator', () => {
// Test specifically the typeof operator
const sourceChapters = [Chapter.SOURCE_1, Chapter.SOURCE_2, Chapter.SOURCE_3, Chapter.SOURCE_4]

// To make sure that typeof is allowed for typed variant
// but not for the default variant
sourceChapters.forEach(chapter => {
testSingleChapter('typeof 0;', "Line 1: Operator 'typeof' is not allowed.", chapter)
testSingleChapter('typeof 0;', undefined, chapter, Variant.TYPED)
})
})
33 changes: 16 additions & 17 deletions src/parser/source/rules/bracesAroundFor.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,12 @@
import { generate } from 'astring'
import * as es from 'estree'
import type { ForStatement } from 'estree'
import { type Rule, RuleError } from '../../types'

import { UNKNOWN_LOCATION } from '../../../constants'
import { ErrorSeverity, ErrorType, Node, Rule, SourceError } from '../../../types'

export class BracesAroundForError implements SourceError {
public type = ErrorType.SYNTAX
public severity = ErrorSeverity.ERROR

constructor(public node: es.ForStatement) {}

get location() {
return this.node.loc ?? UNKNOWN_LOCATION
}
const errorMsg = 'Missing curly braces around "for" block.'

export class BracesAroundForError extends RuleError<ForStatement> {
public explain() {
return 'Missing curly braces around "for" block.'
return errorMsg
}

public elaborate() {
@@ -29,11 +20,19 @@ export class BracesAroundForError implements SourceError {
}
}

const bracesAroundFor: Rule<es.ForStatement> = {
const bracesAroundFor: Rule<ForStatement> = {
name: 'braces-around-for',

testSnippets: [
[
`
let j = 0;
for (let i = 0; i < 1; i = i + 1) j = j + 1;
`,
errorMsg
]
],
checkers: {
ForStatement(node: es.ForStatement, _ancestors: [Node]) {
ForStatement(node: ForStatement) {
if (node.body.type !== 'BlockStatement') {
return [new BracesAroundForError(node)]
} else {
34 changes: 19 additions & 15 deletions src/parser/source/rules/bracesAroundIfElse.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
import { generate } from 'astring'
import * as es from 'estree'

import { UNKNOWN_LOCATION } from '../../../constants'
import { ErrorSeverity, ErrorType, Node, Rule, SourceError } from '../../../types'
import type { IfStatement } from 'estree'
import type { SourceError } from '../../../types'
import { type Rule, RuleError } from '../../types'
import { stripIndent } from '../../../utils/formatters'

export class BracesAroundIfElseError implements SourceError {
public type = ErrorType.SYNTAX
public severity = ErrorSeverity.ERROR

constructor(public node: es.IfStatement, private branch: 'consequent' | 'alternate') {}

get location() {
return this.node.loc ?? UNKNOWN_LOCATION
export class BracesAroundIfElseError extends RuleError<IfStatement> {
constructor(public node: IfStatement, private branch: 'consequent' | 'alternate') {
super(node)
}

public explain() {
@@ -69,11 +63,21 @@ export class BracesAroundIfElseError implements SourceError {
}
}

const bracesAroundIfElse: Rule<es.IfStatement> = {
const bracesAroundIfElse: Rule<IfStatement> = {
name: 'braces-around-if-else',

testSnippets: [
[
`
function f() {
if (true) return false;
else return true;
}
`,
'Line 3: Missing curly braces around "if" block.'
]
],
checkers: {
IfStatement(node: es.IfStatement, _ancestors: [Node]) {
IfStatement(node: IfStatement) {
const errors: SourceError[] = []
if (node.consequent && node.consequent.type !== 'BlockStatement') {
errors.push(new BracesAroundIfElseError(node, 'consequent'))
31 changes: 14 additions & 17 deletions src/parser/source/rules/bracesAroundWhile.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,8 @@
import { generate } from 'astring'
import * as es from 'estree'

import { UNKNOWN_LOCATION } from '../../../constants'
import { ErrorSeverity, ErrorType, Node, Rule, SourceError } from '../../../types'

export class BracesAroundWhileError implements SourceError {
public type = ErrorType.SYNTAX
public severity = ErrorSeverity.ERROR

constructor(public node: es.WhileStatement) {}

get location() {
return this.node.loc ?? UNKNOWN_LOCATION
}
import type { WhileStatement } from 'estree'
import { type Rule, RuleError } from '../../types'

export class BracesAroundWhileError extends RuleError<WhileStatement> {
public explain() {
return 'Missing curly braces around "while" block.'
}
@@ -26,11 +15,19 @@ export class BracesAroundWhileError implements SourceError {
}
}

const bracesAroundWhile: Rule<es.WhileStatement> = {
const bracesAroundWhile: Rule<WhileStatement> = {
name: 'braces-around-while',

testSnippets: [
[
`
let i = 0;
while (true) i = i + 1;
`,
'Line 3: Missing curly braces around "while" block.'
]
],
checkers: {
WhileStatement(node: es.WhileStatement, _ancestors: [Node]) {
WhileStatement(node: WhileStatement) {
if (node.body.type !== 'BlockStatement') {
return [new BracesAroundWhileError(node)]
} else {
27 changes: 12 additions & 15 deletions src/parser/source/rules/forStatementMustHaveAllParts.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
import * as es from 'estree'

import { UNKNOWN_LOCATION } from '../../../constants'
import { ErrorSeverity, ErrorType, Rule, SourceError } from '../../../types'
import type { ForStatement } from 'estree'
import { type Rule, RuleError } from '../../types'
import { stripIndent } from '../../../utils/formatters'

export class ForStatmentMustHaveAllParts implements SourceError {
public type = ErrorType.SYNTAX
public severity = ErrorSeverity.ERROR

constructor(public node: es.ForStatement, private missingParts: string[]) {}

get location() {
return this.node.loc ?? UNKNOWN_LOCATION
export class ForStatmentMustHaveAllParts extends RuleError<ForStatement> {
constructor(public node: ForStatement, private missingParts: string[]) {
super(node)
}

public explain() {
@@ -27,11 +20,15 @@ export class ForStatmentMustHaveAllParts implements SourceError {
}
}

const forStatementMustHaveAllParts: Rule<es.ForStatement> = {
const forStatementMustHaveAllParts: Rule<ForStatement> = {
name: 'for-statement-must-have-all-parts',

testSnippets: [
['let i = 0; for (; i < 0; i = i + 1) {}', 'Line 1: Missing init expression in for statement.'],
['for (let i = 0; ; i = i + 1) {}', 'Line 1: Missing test expression in for statement.'],
['for (let i = 0; i < 0;) {}', 'Line 1: Missing update expression in for statement']
],
checkers: {
ForStatement(node: es.ForStatement) {
ForStatement(node: ForStatement) {
const missingParts = ['init', 'test', 'update'].filter(part => node[part] === null)
if (missingParts.length > 0) {
return [new ForStatmentMustHaveAllParts(node, missingParts)]
5 changes: 2 additions & 3 deletions src/parser/source/rules/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Node, Rule } from '../../../types'
import type { Node } from '../../../types'
import type { Rule } from '../../types'
import bracesAroundFor from './bracesAroundFor'
import bracesAroundIfElse from './bracesAroundIfElse'
import bracesAroundWhile from './bracesAroundWhile'
@@ -10,7 +11,6 @@ import noExportNamedDeclarationWithDefault from './noExportNamedDeclarationWithD
import noFunctionDeclarationWithoutIdentifier from './noFunctionDeclarationWithoutIdentifier'
import noHolesInArrays from './noHolesInArrays'
import noIfWithoutElse from './noIfWithoutElse'
import noImplicitDeclareUndefined from './noImplicitDeclareUndefined'
import noImplicitReturnUndefined from './noImplicitReturnUndefined'
import noImportSpecifierWithDefault from './noImportSpecifierWithDefault'
import noNull from './noNull'
@@ -34,7 +34,6 @@ const rules: Rule<Node>[] = [
noFunctionDeclarationWithoutIdentifier,
noIfWithoutElse,
noImportSpecifierWithDefault,
noImplicitDeclareUndefined,
noImplicitReturnUndefined,
noNull,
noUnspecifiedLiteral,
29 changes: 11 additions & 18 deletions src/parser/source/rules/noDeclareMutable.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,34 @@
import { generate } from 'astring'
import * as es from 'estree'

import { UNKNOWN_LOCATION } from '../../../constants'
import { Chapter, ErrorSeverity, ErrorType, Node, Rule, SourceError } from '../../../types'
import type { VariableDeclaration } from 'estree'
import { Chapter } from '../../../types'
import { type Rule, RuleError } from '../../types'
import { getVariableDeclarationName } from '../../../utils/ast/astCreator'

const mutableDeclarators = ['let', 'var']

export class NoDeclareMutableError implements SourceError {
public type = ErrorType.SYNTAX
public severity = ErrorSeverity.ERROR

constructor(public node: es.VariableDeclaration) {}

get location() {
return this.node.loc ?? UNKNOWN_LOCATION
}

export class NoDeclareMutableError extends RuleError<VariableDeclaration> {
public explain() {
return (
'Mutable variable declaration using keyword ' + `'${this.node.kind}'` + ' is not allowed.'
)
}

public elaborate() {
const name = (this.node.declarations[0].id as es.Identifier).name
const name = getVariableDeclarationName(this.node)
const value = generate(this.node.declarations[0].init)

return `Use keyword "const" instead, to declare a constant:\n\n\tconst ${name} = ${value};`
}
}

const noDeclareMutable: Rule<es.VariableDeclaration> = {
const noDeclareMutable: Rule<VariableDeclaration> = {
name: 'no-declare-mutable',
disableFromChapter: Chapter.SOURCE_3,

testSnippets: [
['let i = 0;', "Line 1: Mutable variable declaration using keyword 'let' is not allowed."]
],
checkers: {
VariableDeclaration(node: es.VariableDeclaration, _ancestors: [Node]) {
VariableDeclaration(node: VariableDeclaration) {
if (mutableDeclarators.includes(node.kind)) {
return [new NoDeclareMutableError(node)]
} else {
35 changes: 17 additions & 18 deletions src/parser/source/rules/noDotAbbreviation.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
import * as es from 'estree'
import type { MemberExpression } from 'estree'
import { Chapter } from '../../../types'
import { type Rule, RuleError } from '../../types'

import { UNKNOWN_LOCATION } from '../../../constants'
import { Chapter, ErrorSeverity, ErrorType, Node, Rule, SourceError } from '../../../types'

export class NoDotAbbreviationError implements SourceError {
public type = ErrorType.SYNTAX
public severity = ErrorSeverity.ERROR

constructor(public node: es.MemberExpression) {}

get location() {
return this.node.loc ?? UNKNOWN_LOCATION
}
const errorMessage = 'Dot abbreviations are not allowed.'

export class NoDotAbbreviationError extends RuleError<MemberExpression> {
public explain() {
return 'Dot abbreviations are not allowed.'
return errorMessage
}

public elaborate() {
@@ -23,13 +15,20 @@ export class NoDotAbbreviationError implements SourceError {
}
}

const noDotAbbreviation: Rule<es.MemberExpression> = {
const noDotAbbreviation: Rule<MemberExpression> = {
name: 'no-dot-abbreviation',

disableFromChapter: Chapter.LIBRARY_PARSER,

testSnippets: [
[
`
const obj = {};
obj.o;
`,
`Line 3: ${errorMessage}`
]
],
checkers: {
MemberExpression(node: es.MemberExpression, _ancestors: [Node]) {
MemberExpression(node: MemberExpression) {
if (!node.computed) {
return [new NoDotAbbreviationError(node)]
} else {
23 changes: 6 additions & 17 deletions src/parser/source/rules/noEval.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,7 @@
import * as es from 'estree'

import { UNKNOWN_LOCATION } from '../../../constants'
import { ErrorSeverity, ErrorType, Node, Rule, SourceError } from '../../../types'

export class NoEval implements SourceError {
public type = ErrorType.SYNTAX
public severity = ErrorSeverity.ERROR

constructor(public node: es.Identifier) {}

get location() {
return this.node.loc ?? UNKNOWN_LOCATION
}
import type { Identifier } from 'estree'
import { type Rule, RuleError } from '../../types'

export class NoEval extends RuleError<Identifier> {
public explain() {
return `eval is not allowed.`
}
@@ -22,11 +11,11 @@ export class NoEval implements SourceError {
}
}

const noEval: Rule<es.Identifier> = {
const noEval: Rule<Identifier> = {
name: 'no-eval',

testSnippets: [['eval("0;");', 'Line 1: eval is not allowed.']],
checkers: {
Identifier(node: es.Identifier, _ancestors: [Node]) {
Identifier(node) {
if (node.name === 'eval') {
return [new NoEval(node)]
} else {
35 changes: 35 additions & 0 deletions src/parser/source/rules/noExportNamedDeclarationReexport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { ExportNamedDeclaration } from 'estree'
import { Chapter } from '../../../types'
import { type Rule, RuleError } from '../../types'

export class NoExportNamedDeclarationReexportError extends RuleError<ExportNamedDeclaration> {
explain() {
return 'Export declarations cannot reexport from another module!'
}
elaborate() {
return 'You cannot use exports of the form \'export {} from "module";\''
}
}

const noExportNamedDeclarationWithSource: Rule<ExportNamedDeclaration> = {
name: 'no-export-named-declaration-with-source',
checkers: {
ExportNamedDeclaration(node) {
return node.source ? [new NoExportNamedDeclarationReexportError(node)] : []
}
},
disableFromChapter: Chapter.LIBRARY_PARSER,
testSnippets: [
[
'export { heart } from "rune";',
'Line 1: Export declarations cannot reexport from another module!'
],
[
'export { heart } from "./a.js";',
'Line 1: Export declarations cannot reexport from another module!'
],
['const heart = 0;\nexport { heart };', undefined]
]
}

export default noExportNamedDeclarationWithSource
35 changes: 16 additions & 19 deletions src/parser/source/rules/noExportNamedDeclarationWithDefault.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,9 @@
import * as es from 'estree'

import { UNKNOWN_LOCATION } from '../../../constants'
import type { ExportNamedDeclaration } from 'estree'
import { defaultExportLookupName } from '../../../stdlib/localImport.prelude'
import { ErrorSeverity, ErrorType, Node, Rule, SourceError } from '../../../types'
import { type Rule, RuleError } from '../../types'
import syntaxBlacklist from '../syntax'

export class NoExportNamedDeclarationWithDefaultError implements SourceError {
public type = ErrorType.SYNTAX
public severity = ErrorSeverity.ERROR

constructor(public node: es.ExportNamedDeclaration) {}

get location() {
return this.node.loc ?? UNKNOWN_LOCATION
}

export class NoExportNamedDeclarationWithDefaultError extends RuleError<ExportNamedDeclaration> {
public explain() {
return 'Export default declarations are not allowed'
}
@@ -24,14 +13,22 @@ export class NoExportNamedDeclarationWithDefaultError implements SourceError {
}
}

const noExportNamedDeclarationWithDefault: Rule<es.ExportNamedDeclaration> = {
name: 'no-declare-mutable',
const noExportNamedDeclarationWithDefault: Rule<ExportNamedDeclaration> = {
name: 'no-default-export',
disableFromChapter: syntaxBlacklist['ExportDefaultDeclaration'],

testSnippets: [
[
`
const a = 0;
export { a as default };
`,
'Line 3: Export default declarations are not allowed'
]
],
checkers: {
ExportNamedDeclaration(node: es.ExportNamedDeclaration, _ancestors: [Node]) {
ExportNamedDeclaration(node) {
const errors: NoExportNamedDeclarationWithDefaultError[] = []
node.specifiers.forEach((specifier: es.ExportSpecifier) => {
node.specifiers.forEach(specifier => {
if (specifier.exported.name === defaultExportLookupName) {
errors.push(new NoExportNamedDeclarationWithDefaultError(node))
}
28 changes: 11 additions & 17 deletions src/parser/source/rules/noFunctionDeclarationWithoutIdentifier.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,7 @@
import * as es from 'estree'

import { UNKNOWN_LOCATION } from '../../../constants'
import { ErrorSeverity, ErrorType, Node, Rule, SourceError } from '../../../types'

export class NoFunctionDeclarationWithoutIdentifierError implements SourceError {
public type = ErrorType.SYNTAX
public severity = ErrorSeverity.ERROR

constructor(public node: es.FunctionDeclaration) {}

get location() {
return this.node.loc ?? UNKNOWN_LOCATION
}
import { FunctionDeclaration } from 'estree'
import { type Rule, RuleError } from '../../types'

export class NoFunctionDeclarationWithoutIdentifierError extends RuleError<FunctionDeclaration> {
public explain() {
return `The 'function' keyword needs to be followed by a name.`
}
@@ -22,11 +11,16 @@ export class NoFunctionDeclarationWithoutIdentifierError implements SourceError
}
}

const noFunctionDeclarationWithoutIdentifier: Rule<es.FunctionDeclaration> = {
const noFunctionDeclarationWithoutIdentifier: Rule<FunctionDeclaration> = {
name: 'no-function-declaration-without-identifier',

testSnippets: [
[
'export default function() {}',
"Line 1: The 'function' keyword needs to be followed by a name."
]
],
checkers: {
FunctionDeclaration(node: es.FunctionDeclaration, _ancestors: Node[]): SourceError[] {
FunctionDeclaration(node) {
if (node.id === null) {
return [new NoFunctionDeclarationWithoutIdentifierError(node)]
}
23 changes: 6 additions & 17 deletions src/parser/source/rules/noHolesInArrays.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,8 @@
import * as es from 'estree'

import { UNKNOWN_LOCATION } from '../../../constants'
import { ErrorSeverity, ErrorType, Rule, SourceError } from '../../../types'
import type { ArrayExpression } from 'estree'
import { type Rule, RuleError } from '../../types'
import { stripIndent } from '../../../utils/formatters'

export class NoHolesInArrays implements SourceError {
public type = ErrorType.SYNTAX
public severity = ErrorSeverity.ERROR

constructor(public node: es.ArrayExpression) {}

get location() {
return this.node.loc ?? UNKNOWN_LOCATION
}

export class NoHolesInArrays extends RuleError<ArrayExpression> {
public explain() {
return `No holes are allowed in array literals.`
}
@@ -26,11 +15,11 @@ export class NoHolesInArrays implements SourceError {
}
}

const noHolesInArrays: Rule<es.ArrayExpression> = {
const noHolesInArrays: Rule<ArrayExpression> = {
name: 'no-holes-in-arrays',

testSnippets: [['[0,,0];', 'Line 1: No holes are allowed in array literals.']],
checkers: {
ArrayExpression(node: es.ArrayExpression) {
ArrayExpression(node) {
return node.elements.some(x => x === null) ? [new NoHolesInArrays(node)] : []
}
}
35 changes: 19 additions & 16 deletions src/parser/source/rules/noIfWithoutElse.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,10 @@
import { generate } from 'astring'
import * as es from 'estree'

import { UNKNOWN_LOCATION } from '../../../constants'
import { Chapter, ErrorSeverity, ErrorType, Node, Rule, SourceError } from '../../../types'
import type { IfStatement } from 'estree'
import { Chapter } from '../../../types'
import { type Rule, RuleError } from '../../types'
import { stripIndent } from '../../../utils/formatters'

export class NoIfWithoutElseError implements SourceError {
public type = ErrorType.SYNTAX
public severity = ErrorSeverity.ERROR

constructor(public node: es.IfStatement) {}

get location() {
return this.node.loc ?? UNKNOWN_LOCATION
}

export class NoIfWithoutElseError extends RuleError<IfStatement> {
public explain() {
return 'Missing "else" in "if-else" statement.'
}
@@ -30,11 +20,24 @@ export class NoIfWithoutElseError implements SourceError {
}
}

const noIfWithoutElse: Rule<es.IfStatement> = {
const noIfWithoutElse: Rule<IfStatement> = {
name: 'no-if-without-else',
disableFromChapter: Chapter.SOURCE_3,
testSnippets: [
[
`
function f() {
if (true) {
return true;
}
return false;
}
`,
'Line 3: Missing "else" in "if-else" statement.'
]
],
checkers: {
IfStatement(node: es.IfStatement, _ancestors: [Node]) {
IfStatement(node) {
if (!node.alternate) {
return [new NoIfWithoutElseError(node)]
} else {
49 changes: 0 additions & 49 deletions src/parser/source/rules/noImplicitDeclareUndefined.ts

This file was deleted.

23 changes: 6 additions & 17 deletions src/parser/source/rules/noImplicitReturnUndefined.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,8 @@
import * as es from 'estree'

import { UNKNOWN_LOCATION } from '../../../constants'
import { ErrorSeverity, ErrorType, Node, Rule, SourceError } from '../../../types'
import type { ReturnStatement } from 'estree'
import { type Rule, RuleError } from '../../types'
import { stripIndent } from '../../../utils/formatters'

export class NoImplicitReturnUndefinedError implements SourceError {
public type = ErrorType.SYNTAX
public severity = ErrorSeverity.ERROR

constructor(public node: es.ReturnStatement) {}

get location() {
return this.node.loc ?? UNKNOWN_LOCATION
}

export class NoImplicitReturnUndefinedError extends RuleError<ReturnStatement> {
public explain() {
return 'Missing value in return statement.'
}
@@ -28,11 +17,11 @@ export class NoImplicitReturnUndefinedError implements SourceError {
}
}

const noImplicitReturnUndefined: Rule<es.ReturnStatement> = {
const noImplicitReturnUndefined: Rule<ReturnStatement> = {
name: 'no-implicit-return-undefined',

testSnippets: [['function f() { return; }', 'Line 1: Missing value in return statement.']],
checkers: {
ReturnStatement(node: es.ReturnStatement, _ancestors: [Node]) {
ReturnStatement(node) {
if (!node.argument) {
return [new NoImplicitReturnUndefinedError(node)]
} else {
25 changes: 8 additions & 17 deletions src/parser/source/rules/noImportSpecifierWithDefault.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,9 @@
import * as es from 'estree'

import { UNKNOWN_LOCATION } from '../../../constants'
import type { ImportSpecifier } from 'estree'
import { defaultExportLookupName } from '../../../stdlib/localImport.prelude'
import { ErrorSeverity, ErrorType, Node, Rule, SourceError } from '../../../types'
import { type Rule, RuleError } from '../../types'
import syntaxBlacklist from '../syntax'

export class NoImportSpecifierWithDefaultError implements SourceError {
public type = ErrorType.SYNTAX
public severity = ErrorSeverity.ERROR

constructor(public node: es.ImportSpecifier) {}

get location() {
return this.node.loc ?? UNKNOWN_LOCATION
}

export class NoImportSpecifierWithDefaultError extends RuleError<ImportSpecifier> {
public explain() {
return 'Import default specifiers are not allowed'
}
@@ -24,12 +13,14 @@ export class NoImportSpecifierWithDefaultError implements SourceError {
}
}

const noImportSpecifierWithDefault: Rule<es.ImportSpecifier> = {
const noImportSpecifierWithDefault: Rule<ImportSpecifier> = {
name: 'no-declare-mutable',
disableFromChapter: syntaxBlacklist['ImportDefaultSpecifier'],

testSnippets: [
['import { default as a } from "./a.js";', 'Line 1: Import default specifiers are not allowed.']
],
checkers: {
ImportSpecifier(node: es.ImportSpecifier, _ancestors: [Node]) {
ImportSpecifier(node) {
if (node.imported.name === defaultExportLookupName) {
return [new NoImportSpecifierWithDefaultError(node)]
}
23 changes: 7 additions & 16 deletions src/parser/source/rules/noNull.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,8 @@
import * as es from 'estree'

import { UNKNOWN_LOCATION } from '../../../constants'
import { Chapter, ErrorSeverity, ErrorType, Node, Rule, SourceError } from '../../../types'

export class NoNullError implements SourceError {
public type = ErrorType.SYNTAX
public severity = ErrorSeverity.ERROR

constructor(public node: es.Literal) {}

get location() {
return this.node.loc ?? UNKNOWN_LOCATION
}
import type { Literal } from 'estree'
import { Chapter } from '../../../types'
import { type Rule, RuleError } from '../../types'

export class NoNullError extends RuleError<Literal> {
public explain() {
return `null literals are not allowed.`
}
@@ -22,11 +12,12 @@ export class NoNullError implements SourceError {
}
}

const noNull: Rule<es.Literal> = {
const noNull: Rule<Literal> = {
name: 'no-null',
disableFromChapter: Chapter.SOURCE_2,
testSnippets: [['null;', 'Line 1: null literals are not allowed.']],
checkers: {
Literal(node: es.Literal, _ancestors: [Node]) {
Literal(node) {
if (node.value === null) {
return [new NoNullError(node)]
} else {
28 changes: 10 additions & 18 deletions src/parser/source/rules/noSpreadInArray.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,7 @@
import * as es from 'estree'

import { UNKNOWN_LOCATION } from '../../../constants'
import { ErrorSeverity, ErrorType, Node, Rule, SourceError } from '../../../types'

export class NoSpreadInArray implements SourceError {
public type = ErrorType.SYNTAX
public severity = ErrorSeverity.ERROR

constructor(public node: es.SpreadElement) {}

get location() {
return this.node.loc ?? UNKNOWN_LOCATION
}
import type { SpreadElement } from 'estree'
import { RuleError, type Rule } from '../../types'

export class NoSpreadInArray extends RuleError<SpreadElement> {
public explain() {
return 'Spread syntax is not allowed in arrays.'
}
@@ -22,11 +11,14 @@ export class NoSpreadInArray implements SourceError {
}
}

const noSpreadInArray: Rule<es.SpreadElement> = {
const noSpreadInArray = {
name: 'no-assignment-expression',

testSnippets: [
['const a = [...b];', 'Line 1: Spread syntax is not allowed in arrays.'],
['display(...args);', undefined]
],
checkers: {
SpreadElement(node: es.SpreadElement, ancestors: [Node]) {
SpreadElement(node, ancestors) {
const parent = ancestors[ancestors.length - 2]

if (parent.type === 'CallExpression') {
@@ -36,6 +28,6 @@ const noSpreadInArray: Rule<es.SpreadElement> = {
}
}
}
}
} satisfies Rule<SpreadElement>

export default noSpreadInArray
29 changes: 12 additions & 17 deletions src/parser/source/rules/noTemplateExpression.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,7 @@
import * as es from 'estree'

import { UNKNOWN_LOCATION } from '../../../constants'
import { ErrorSeverity, ErrorType, Node, Rule, SourceError } from '../../../types'

export class NoTemplateExpressionError implements SourceError {
public type = ErrorType.SYNTAX
public severity = ErrorSeverity.ERROR

constructor(public node: es.TemplateLiteral) {}

get location() {
return this.node.loc ?? UNKNOWN_LOCATION
}
import type { TemplateLiteral } from 'estree'
import { type Rule, RuleError } from '../../types'

export class NoTemplateExpressionError extends RuleError<TemplateLiteral> {
public explain() {
return 'Expressions are not allowed in template literals (`multiline strings`)'
}
@@ -22,11 +11,17 @@ export class NoTemplateExpressionError implements SourceError {
}
}

const noTemplateExpression: Rule<es.TemplateLiteral> = {
const noTemplateExpression: Rule<TemplateLiteral> = {
name: 'no-template-expression',

testSnippets: [
['`\n`;', undefined],
[
'const x = 0; `${x}`;',
'Line 1: Expressions are not allowed in template literals (`multiline strings`)'
]
],
checkers: {
TemplateLiteral(node: es.TemplateLiteral, _ancestors: [Node]) {
TemplateLiteral(node) {
if (node.expressions.length > 0) {
return [new NoTemplateExpressionError(node)]
} else {
12 changes: 6 additions & 6 deletions src/parser/source/rules/noTypeofOperator.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import * as es from 'estree'

import { Rule, Variant } from '../../../types'
import type { UnaryExpression } from 'estree'
import { Variant } from '../../../types'
import type { Rule } from '../../types'
import { NoUnspecifiedOperatorError } from './noUnspecifiedOperator'

const noTypeofOperator: Rule<es.UnaryExpression> = {
const noTypeofOperator: Rule<UnaryExpression> = {
name: 'no-typeof-operator',
disableForVariants: [Variant.TYPED],

testSnippets: [['typeof "string";', "Line 1: Operator 'typeof' is not allowed."]],
checkers: {
UnaryExpression(node: es.UnaryExpression) {
UnaryExpression(node) {
if (node.operator === 'typeof') {
return [new NoUnspecifiedOperatorError(node)]
} else {
22 changes: 6 additions & 16 deletions src/parser/source/rules/noUnspecifiedLiteral.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,9 @@
import * as es from 'estree'

import { UNKNOWN_LOCATION } from '../../../constants'
import { ErrorSeverity, ErrorType, Node, Rule, SourceError } from '../../../types'
import type { Literal } from 'estree'
import { type Rule, RuleError } from '../../types'

const specifiedLiterals = ['boolean', 'string', 'number']

export class NoUnspecifiedLiteral implements SourceError {
public type = ErrorType.SYNTAX
public severity = ErrorSeverity.ERROR

constructor(public node: es.Literal) {}

get location() {
return this.node.loc ?? UNKNOWN_LOCATION
}

export class NoUnspecifiedLiteral extends RuleError<Literal> {
public explain() {
/**
* A check is used for RegExp to ensure that only RegExp are caught.
@@ -29,10 +18,11 @@ export class NoUnspecifiedLiteral implements SourceError {
}
}

const noUnspecifiedLiteral: Rule<es.Literal> = {
const noUnspecifiedLiteral: Rule<Literal> = {
name: 'no-unspecified-literal',
testSnippets: [['const x = /hi/;', "Line 1: 'RegExp' literals are not allowed."]],
checkers: {
Literal(node: es.Literal, _ancestors: [Node]) {
Literal(node) {
if (node.value !== null && !specifiedLiterals.includes(typeof node.value)) {
return [new NoUnspecifiedLiteral(node)]
} else {
240 changes: 196 additions & 44 deletions src/parser/source/rules/noUnspecifiedOperator.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,148 @@
import * as es from 'estree'
import type es from 'estree'

import { UNKNOWN_LOCATION } from '../../../constants'
import { ErrorSeverity, ErrorType, Node, Rule, SourceError } from '../../../types'
import { generate } from 'astring'
import { RuleError, type Rule } from '../../types'
import type { SourceError } from '../../../types'

export class NoUnspecifiedOperatorError implements SourceError {
public type = ErrorType.SYNTAX
public severity = ErrorSeverity.ERROR
public unspecifiedOperator: string
type OperatorNodeTypes =
| es.BinaryExpression
| es.UnaryExpression
| es.LogicalExpression
| es.AssignmentExpression

constructor(public node: es.BinaryExpression | es.UnaryExpression) {
this.unspecifiedOperator = node.operator
type OperatorRecord<T extends OperatorNodeTypes> = {
/**
* Array of allowed operators
*/
allowed: T['operator'][]

/**
* Array of disallowed operators
*/
disallowed: T['operator'][]

/**
* Function used to map allowed operators to test snippets
*/
allowedSnippetMapper: (ops: T['operator'][]) => string

/**
* Function used to map disallowed operators to test snippets
*/
disallowedSnippetMapper: (ops: T['operator'][]) => [string, string]

/**
* Checking function to use with the given node type
*/
checker: (node: T, ancestors: Node[]) => SourceError[]
}

type OperatorClassifications = {
[K in OperatorNodeTypes['type']]: OperatorRecord<Extract<OperatorNodeTypes, { type: K }>>
}

const disallowedBinaryOperators: es.BinaryOperator[] = [
// '==',
// '!=',
// "**",
'|',
'^',
'&',
'in',
'instanceof',
// '??',
'>>',
'<<',
'>>>'
]

const operators: OperatorClassifications = {
AssignmentExpression: {
allowed: ['='],
disallowed: [
// Some operators aren't considered as valid operators by estree
'+=',
'-=',
'*=',
'/=',
'%=',
// "**=",
'<<=',
'>>=',
'>>>=',
'|=',
'^=',
'&='
// "||=",
// "&&=",
// "??="
],
allowedSnippetMapper: op => `a ${op} b`,
disallowedSnippetMapper: op => [
`a ${op} b;`,
`Line 1: The assignment operator ${op} is not allowed. Use = instead.`
],
checker(node) {
if (node.operator !== '=') return [new NoUpdateAssignment(node)]

const op = node.operator.slice(0, -1) as es.BinaryOperator
if (disallowedBinaryOperators.includes(op)) {
return [new NoUnspecifiedOperatorError(node)]
}

return []
}
},
BinaryExpression: {
disallowed: disallowedBinaryOperators,
allowed: ['+', '-', '*', '/', '%', '===', '!==', '<', '>', '<=', '>='],
allowedSnippetMapper: op => `a ${op} b;`,
disallowedSnippetMapper: op => [`a ${op} b;`, `Operator '${op}' is not allowed.`],
checker(node) {
if (node.operator === '==' || node.operator === '!=') {
return [new StrictEqualityError(node)]
} else if (this.disallowed.includes(node.operator)) {
return [new NoUnspecifiedOperatorError(node)]
}

return []
}
},
LogicalExpression: {
allowed: ['||', '&&'],
disallowed: ['??'],
allowedSnippetMapper: op => `a ${op} b;`,
disallowedSnippetMapper: op => [`a ${op} b;`, `Operator '${op}' is not allowed.`],
checker(node) {
if (this.disallowed.includes(node.operator)) {
return [new NoUnspecifiedOperatorError(node)]
} else {
return []
}
}
},
UnaryExpression: {
allowed: ['-', '!', 'typeof'],
disallowed: ['~', '+', 'void'],
allowedSnippetMapper: op => `${op} a;`,
disallowedSnippetMapper: op => [`${op} a;`, `Operator '${op}' is not allowed.`],
checker(node) {
if (this.disallowed.includes(node.operator)) {
return [new NoUnspecifiedOperatorError(node)]
}
return []
}
}
}

get location() {
return this.node.loc ?? UNKNOWN_LOCATION
export class NoUnspecifiedOperatorError<
T extends OperatorNodeTypes = OperatorNodeTypes
> extends RuleError<T> {
public unspecifiedOperator: T['operator']

constructor(node: T) {
super(node)
this.unspecifiedOperator = node.operator
}

public explain() {
@@ -25,41 +154,64 @@ export class NoUnspecifiedOperatorError implements SourceError {
}
}

const noUnspecifiedOperator: Rule<es.BinaryExpression | es.UnaryExpression> = {
name: 'no-unspecified-operator',

checkers: {
BinaryExpression(node: es.BinaryExpression, _ancestors: [Node]) {
const permittedOperators = [
'+',
'-',
'*',
'/',
'%',
'===',
'!==',
'<',
'>',
'<=',
'>=',
'&&',
'||'
]
if (!permittedOperators.includes(node.operator)) {
return [new NoUnspecifiedOperatorError(node)]
} else {
return []
}
},
UnaryExpression(node: es.UnaryExpression) {
const permittedOperators = ['-', '!', 'typeof']
if (!permittedOperators.includes(node.operator)) {
return [new NoUnspecifiedOperatorError(node)]
} else {
return []
}
export class NoUpdateAssignment extends NoUnspecifiedOperatorError<es.AssignmentExpression> {
public override explain() {
return `The assignment operator ${this.node.operator} is not allowed. Use = instead.`
}

public override elaborate() {
const leftStr = generate(this.node.left)
const rightStr = generate(this.node.right)
const opStr = this.node.operator.slice(0, -1)

return `\n\t${leftStr} = ${leftStr} ${opStr} ${rightStr};`
}
}

export class StrictEqualityError extends NoUnspecifiedOperatorError<es.BinaryExpression> {
public override explain() {
if (this.node.operator === '==') {
return 'Use === instead of ==.'
} else {
return 'Use !== instead of !=.'
}
}

public override elaborate() {
return '== and != are not valid operators.'
}
}

const noUnspecifiedOperator = Object.entries(operators).reduce(
(res, [nodeType, ruleObj]) => {
const { checker, allowed, disallowed, allowedSnippetMapper, disallowedSnippetMapper } = ruleObj

const allowedSnippets = allowed.map(each => {
// type intersection gets narrowed down to never, so we manually suppress the ts error
// @ts-expect-error 2345
return [allowedSnippetMapper(each), undefined] as [string, undefined]
})

// @ts-expect-error 2345
const disallowedSnippets = disallowed.map(disallowedSnippetMapper)

return {
...res,
testSnippets: [...res.testSnippets!, ...disallowedSnippets, ...allowedSnippets],
checkers: {
...res.checkers,
[nodeType]: checker.bind(ruleObj)
}
}
},
{
name: 'no-unspecified-operator',
testSnippets: [
['a == b', 'Line 1: Use === instead of =='],
['a != b', 'Line 1: Use !== instead of !=']
],
checkers: {}
} as Rule<OperatorNodeTypes>
)

export default noUnspecifiedOperator
21 changes: 5 additions & 16 deletions src/parser/source/rules/noUpdateAssignment.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,8 @@
import { generate } from 'astring'
import * as es from 'estree'

import { UNKNOWN_LOCATION } from '../../../constants'
import { ErrorSeverity, ErrorType, Node, Rule, SourceError } from '../../../types'

export class NoUpdateAssignment implements SourceError {
public type = ErrorType.SYNTAX
public severity = ErrorSeverity.ERROR

constructor(public node: es.AssignmentExpression) {}

get location() {
return this.node.loc ?? UNKNOWN_LOCATION
}
import type { AssignmentExpression } from 'estree'
import { type Rule, RuleError } from '../../types'

export class NoUpdateAssignment extends RuleError<AssignmentExpression> {
public explain() {
return 'The assignment operator ' + this.node.operator + ' is not allowed. Use = instead.'
}
@@ -33,11 +22,11 @@ export class NoUpdateAssignment implements SourceError {
}
}

const noUpdateAssignment: Rule<es.AssignmentExpression> = {
const noUpdateAssignment: Rule<AssignmentExpression> = {
name: 'no-update-assignment',

checkers: {
AssignmentExpression(node: es.AssignmentExpression, _ancestors: [Node]) {
AssignmentExpression(node) {
if (node.operator !== '=') {
return [new NoUpdateAssignment(node)]
} else {
26 changes: 8 additions & 18 deletions src/parser/source/rules/noVar.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,26 @@
import { generate } from 'astring'
import * as es from 'estree'

import { UNKNOWN_LOCATION } from '../../../constants'
import { ErrorSeverity, ErrorType, Node, Rule, SourceError } from '../../../types'

export class NoVarError implements SourceError {
public type = ErrorType.SYNTAX
public severity = ErrorSeverity.ERROR

constructor(public node: es.VariableDeclaration) {}

get location() {
return this.node.loc ?? UNKNOWN_LOCATION
}
import type { VariableDeclaration } from 'estree'
import { type Rule, RuleError } from '../../types'
import { getVariableDeclarationName } from '../../../utils/ast/astCreator'

export class NoVarError extends RuleError<VariableDeclaration> {
public explain() {
return 'Variable declaration using "var" is not allowed.'
}

public elaborate() {
const name = (this.node.declarations[0].id as es.Identifier).name
const name = getVariableDeclarationName(this.node)
const value = generate(this.node.declarations[0].init)

return `Use keyword "let" instead, to declare a variable:\n\n\tlet ${name} = ${value};`
}
}

const noVar: Rule<es.VariableDeclaration> = {
const noVar: Rule<VariableDeclaration> = {
name: 'no-var',

testSnippets: [['var x = 0;', 'Line 1: Variable declaration using "var" is not allowed.']],
checkers: {
VariableDeclaration(node: es.VariableDeclaration, _ancestors: [Node]) {
VariableDeclaration(node) {
if (node.kind === 'var') {
return [new NoVarError(node)]
} else {
65 changes: 49 additions & 16 deletions src/parser/source/rules/singleVariableDeclaration.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,40 @@
import { generate } from 'astring'
import * as es from 'estree'
import type { Identifier, VariableDeclaration, VariableDeclarator } from 'estree'

import { UNKNOWN_LOCATION } from '../../../constants'
import { ErrorSeverity, ErrorType, Node, Rule, SourceError } from '../../../types'
import { RuleError, type Rule } from '../../types'
import { stripIndent } from '../../../utils/formatters'
import { mapAndFilter } from '../../../utils/misc'

export class MultipleDeclarationsError implements SourceError {
public type = ErrorType.SYNTAX
public severity = ErrorSeverity.ERROR
private fixs: es.VariableDeclaration[]
export class NoImplicitDeclareUndefinedError extends RuleError<VariableDeclarator> {
private readonly name: string

constructor(node: VariableDeclarator) {
super(node)
this.name = (node.id as Identifier).name
}

public explain() {
return 'Missing value in variable declaration.'
}

public elaborate() {
return stripIndent`
A variable declaration assigns a value to a name.
For instance, to assign 20 to ${this.name}, you can write:
let ${this.name} = 20;
${this.name} + ${this.name}; // 40
`
}
}

export class MultipleDeclarationsError extends RuleError<VariableDeclaration> {
private fixs: VariableDeclaration[]

constructor(node: VariableDeclaration) {
super(node)

constructor(public node: es.VariableDeclaration) {
this.fixs = node.declarations.map(declaration => ({
type: 'VariableDeclaration' as const,
kind: 'let' as const,
@@ -18,12 +43,8 @@ export class MultipleDeclarationsError implements SourceError {
}))
}

get location() {
return this.node.loc ?? UNKNOWN_LOCATION
}

public explain() {
return 'Multiple declaration in a single statement.'
return 'Multiple declarations in a single statement.'
}

public elaborate() {
@@ -32,16 +53,28 @@ export class MultipleDeclarationsError implements SourceError {
}
}

const singleVariableDeclaration: Rule<es.VariableDeclaration> = {
const singleVariableDeclaration: Rule<VariableDeclaration> = {
name: 'single-variable-declaration',
testSnippets: [
['let i = 0, j = 0;', 'Line 1: Multiple declarations in a single statement.'],
['let i;', 'Line 1: Missing value in variable declaration.'],
['for (const x of []) {}', undefined]
],

checkers: {
VariableDeclaration(node: es.VariableDeclaration, _ancestors: [Node]) {
VariableDeclaration(node, ancestors) {
if (node.declarations.length > 1) {
return [new MultipleDeclarationsError(node)]
} else {
}

const ancestor = ancestors[ancestors.length - 2]
if (ancestor.type === 'ForOfStatement' || ancestor.type === 'ForInStatement') {
return []
}

return mapAndFilter(node.declarations, decl =>
decl.init ? undefined : new NoImplicitDeclareUndefinedError(decl)
)
}
}
}
43 changes: 0 additions & 43 deletions src/parser/source/rules/strictEquality.ts

This file was deleted.

42 changes: 36 additions & 6 deletions src/parser/types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { ParserOptions } from '@babel/parser'
import { Options } from 'acorn'
import { Program } from 'estree'
import type { Program } from 'estree'

import { Context } from '../types'
import { Chapter, Context, ErrorSeverity, ErrorType, Node, SourceError, Variant } from '../types'
import { UNKNOWN_LOCATION } from '../constants'

export type AcornOptions = Options
export type BabelOptions = ParserOptions
export type { ParserOptions as BabelOptions } from '@babel/parser'
export type { Options as AcornOptions } from 'acorn'

export interface Parser<TOptions> {
parse(
@@ -16,3 +15,34 @@ export interface Parser<TOptions> {
): Program | null
validate(ast: Program, context: Context, throwOnError?: boolean): boolean
}

export interface Rule<T extends Node> {
name: string
disableFromChapter?: Chapter
disableForVariants?: Variant[]
checkers: {
[name: string]: (node: T, ancestors: Node[]) => SourceError[]
}
/**
* Test snippets to test the behaviour of the rule. Providing no test snippets
* means that this rule will not be tested when running unit tests.\
* First element of the tuple is the code to test. Set the second element to `undefined`
* if the snippet should not throw an error. Otherwise set it to the `explain()` value
* of the error.
*/
testSnippets?: [code: string, expected: string | undefined][]
}

export abstract class RuleError<T extends Node> implements SourceError {
public readonly type = ErrorType.SYNTAX
public readonly severity = ErrorSeverity.ERROR

constructor(public readonly node: T) {}

public get location() {
return this.node.loc ?? UNKNOWN_LOCATION
}

public abstract explain(): string
public abstract elaborate(): string
}
9 changes: 0 additions & 9 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -47,15 +47,6 @@ export interface SourceError {
elaborate(): string
}

export interface Rule<T extends Node> {
name: string
disableFromChapter?: Chapter
disableForVariants?: Variant[]
checkers: {
[name: string]: (node: T, ancestors: Node[]) => SourceError[]
}
}

export interface Comment {
type: 'Line' | 'Block'
value: string
2 changes: 2 additions & 0 deletions src/utils/misc.ts
Original file line number Diff line number Diff line change
@@ -42,3 +42,5 @@ export function objectKeys<T extends string | number | symbol>(obj: Record<T, an
export function objectValues<T>(obj: Record<any, T>) {
return Object.values(obj) as T[]
}
export const mapToObj = <T>(map: Map<string, T>) =>
Array.from(map).reduce((obj, [k, v]) => Object.assign(obj, { [k]: v }), {})

Unchanged files with check annotations Beta

* @returns The corresponding promise.
*/
export function CSEResultPromise(context: Context, value: Value): Promise<Result> {
return new Promise((resolve, reject) => {

Check warning on line 332 in src/cse-machine/interpreter.ts

GitHub Actions / build

'reject' is defined but never used. Allowed unused args must match /^_/u
if (value instanceof CSEBreak) {
resolve({ status: 'suspended-cse-eval', context })
} else if (value instanceof CseError) {
command: es.WhileStatement,
context: Context,
control: Control,
stash: Stash

Check warning on line 567 in src/cse-machine/interpreter.ts

GitHub Actions / build

'stash' is defined but never used. Allowed unused args must match /^_/u
) {
if (hasBreakStatement(command.body as es.BlockStatement)) {
control.push(instr.breakMarkerInstr(command))
command: es.IfStatement,
context: Context,
control: Control,
stash: Stash

Check warning on line 648 in src/cse-machine/interpreter.ts

GitHub Actions / build

'stash' is defined but never used. Allowed unused args must match /^_/u
) {
control.push(...reduceConditional(command))
},
command: es.ContinueStatement,
context: Context,
control: Control,
stash: Stash

Check warning on line 721 in src/cse-machine/interpreter.ts

GitHub Actions / build

'stash' is defined but never used. Allowed unused args must match /^_/u
) {
control.push(instr.contInstr(command))
},
command: es.BreakStatement,
context: Context,
control: Control,
stash: Stash

Check warning on line 730 in src/cse-machine/interpreter.ts

GitHub Actions / build

'stash' is defined but never used. Allowed unused args must match /^_/u
) {
control.push(instr.breakInstr(command))
},
command: es.MemberExpression,
context: Context,
control: Control,
stash: Stash

Check warning on line 783 in src/cse-machine/interpreter.ts

GitHub Actions / build

'stash' is defined but never used. Allowed unused args must match /^_/u
) {
control.push(instr.arrAccInstr(command))
control.push(command.property)
command: es.ConditionalExpression,
context: Context,
control: Control,
stash: Stash

Check warning on line 794 in src/cse-machine/interpreter.ts

GitHub Actions / build

'stash' is defined but never used. Allowed unused args must match /^_/u
) {
control.push(...reduceConditional(command))
},
* Instructions
*/
[InstrType.RESET]: function (command: Instr, context: Context, control: Control, stash: Stash) {

Check warning on line 857 in src/cse-machine/interpreter.ts

GitHub Actions / build

'stash' is defined but never used. Allowed unused args must match /^_/u
// Keep pushing reset instructions until marker is found.
const cmdNext: ControlItem | undefined = control.pop()
if (cmdNext && (!isInstr(cmdNext) || cmdNext.instrType !== InstrType.MARKER)) {
-1
)
// Run the new CSE Machine fully to obtain the result in the stash
for (const _ of gen) {

Check warning on line 55 in src/cse-machine/closure.ts

GitHub Actions / build

'_' is assigned a value but never used
}
// Also don't forget to update object count in original context
context.runtime.objectCount = newContext.runtime.objectCount
'call_cc(f)',
context.variant === Variant.EXPLICIT_CONTROL
? call_with_current_continuation
: (f: any) => {

Check warning on line 382 in src/createContext.ts

GitHub Actions / build

'f' is defined but never used. Allowed unused args must match /^_/u
throw new Error('call_cc is only available in Explicit-Control variant')
}
)