@gobstones/gobstones-parser
Version:
Gobstones parser
1,095 lines (998 loc) • 38.9 kB
text/typescript
/* eslint-disable camelcase */
/* eslint-disable no-underscore-dangle */
import {
ASTMain,
ASTStmtBlock,
N_DefFunction,
N_DefInteractiveProgram,
N_DefProcedure,
N_DefProgram,
N_DefType,
N_ExprChoose,
N_ExprConstantNumber,
N_ExprConstantString,
N_ExprFunctionCall,
N_ExprList,
N_ExprMatching,
N_ExprRange,
N_ExprStructure,
N_ExprStructureUpdate,
N_ExprTuple,
N_ExprVariable,
N_PatternNumber,
N_PatternStructure,
N_PatternTimeout,
N_PatternTuple,
N_PatternVariable,
N_PatternWildcard,
N_StmtAssignTuple,
N_StmtAssignVariable,
N_StmtBlock,
N_StmtForeach,
N_StmtIf,
N_StmtProcedureCall,
N_StmtRepeat,
N_StmtReturn,
N_StmtSwitch,
N_StmtWhile
} from './ast';
import { LocalIndex, LocalParameter, LocalVariable, SymbolTable } from './symtable';
import { i18n, i18nF } from './i18n';
import { GbsSyntaxError } from './exceptions';
import { RecursionChecker } from './recursion_checker';
import { SourceReader } from './reader';
// Only for typing purposes
// eslint-disable-next-line @typescript-eslint/ban-types
const toStr = (x: string | Function): string => x as string;
const isBlockWithReturn = (stmt: ASTStmtBlock): boolean =>
stmt.tag === N_StmtBlock &&
stmt.statements.length > 0 &&
stmt.statements.slice(-1)[0].tag === N_StmtReturn;
function fail(startPos: SourceReader, endPos: SourceReader, reason: string, args: any[]): void {
throw new GbsSyntaxError(startPos, endPos, reason, args);
}
/* A semantic analyzer receives
* a symbol table (instance of SymbolTable)
* an abstract syntax tree (the output of a parser)
*
* Then:
*
* - It performs semantic checks (linting) to ensure that the
* program is well-formed.
*
* - It builds a symbol table with information on global identifiers
* such as procedures, functions, types, constructors, and fields.
*
* - The semantic analysis is structured as a recursive visit over the
* AST.
*
* We assume that the AST is the valid output of a parser.
*/
export class Linter {
private _symtable: SymbolTable;
private _enabledLinterChecks: Record<string, boolean>;
public constructor(symtable: SymbolTable) {
this._symtable = symtable;
/* All checks performed by the linter have an entry in this dictionary.
* The value of a check indicates whether it is enabled (true) or
* disabled (false).
*
* If a check is disabled, it does not produce a syntax error.
*/
this._enabledLinterChecks = {
// Linter options
'source-should-have-a-program-definition': true,
'procedure-should-not-have-return': true,
'function-should-have-return': true,
'return-statement-not-allowed-here': true,
'wildcard-pattern-should-be-last': true,
'variable-pattern-should-be-last': true,
'structure-pattern-repeats-constructor': true,
'structure-pattern-repeats-tuple-arity': true,
'structure-pattern-repeats-timeout': true,
'pattern-does-not-match-type': true,
'patterns-in-interactive-program-must-be-events': true,
'patterns-in-interactive-program-cannot-be-variables': true,
'patterns-in-switch-must-not-be-events': true,
'patterns-in-foreach-must-not-be-events': true,
'repeated-variable-in-tuple-assignment': true,
'constructor-used-as-procedure': true,
'undefined-procedure': true,
'procedure-arity-mismatch': true,
'numeric-pattern-repeats-number': true,
'structure-pattern-arity-mismatch': true,
'structure-construction-repeated-field': true,
'structure-construction-invalid-field': true,
'structure-construction-missing-field': true,
'structure-construction-cannot-be-an-event': true,
'undefined-function': true,
'function-arity-mismatch': true,
'type-used-as-constructor': true,
'procedure-used-as-constructor': true,
'undeclared-constructor': true,
// Extensions
'forbidden-extension-destructuring-foreach': true,
'forbidden-extension-allow-recursion': true
};
}
public lint(ast: ASTMain): SymbolTable {
this._lintMain(ast);
return this._symtable;
}
public _ensureLintCheckExists(linterCheckId: string): void {
if (!(linterCheckId in this._enabledLinterChecks)) {
throw Error('Linter check "' + linterCheckId + '" does not exist.');
}
}
public enableCheck(linterCheckId: string, enabled: boolean): void {
this._ensureLintCheckExists(linterCheckId);
this._enabledLinterChecks[linterCheckId] = enabled;
}
public _lintCheck(
startPos: SourceReader,
endPos: SourceReader,
reason: string,
args: any[]
): void {
this._ensureLintCheckExists(reason);
if (this._enabledLinterChecks[reason]) {
fail(startPos, endPos, reason, args);
}
}
public _lintMain(ast: ASTMain): void {
/* Collect all definitions into the symbol table.
* This should be done all together, before linting individual
* definitions, so all the names of types, constructors, fields, etc.
* are already known when checking statements and expressions. */
for (const definition of ast.definitions) {
this._addDefinitionToSymbolTable(definition);
}
/* The source should either be empty or have exactly one program */
if (ast.definitions.length > 0 && this._symtable.program === undefined) {
this._lintCheck(
ast.startPos,
ast.endPos,
'source-should-have-a-program-definition',
[]
);
}
/* Lint individual definitions */
for (const definition of ast.definitions) {
this._lintDefinition(definition);
}
/* Disable recursion */
this._disableRecursion(ast);
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
public _addDefinitionToSymbolTable(definition: any): void {
switch (definition.tag) {
case N_DefProgram:
return this._symtable.defProgram(definition);
case N_DefInteractiveProgram:
return this._symtable.defInteractiveProgram(definition);
case N_DefProcedure:
return this._symtable.defProcedure(definition);
case N_DefFunction:
return this._symtable.defFunction(definition);
case N_DefType:
return this._symtable.defType(definition);
default:
throw Error('Unknown definition: ' + Symbol.keyFor(definition.tag));
}
}
/** Definitions **/
public _lintDefinition(definition: { tag: symbol }): void {
switch (definition.tag) {
case N_DefProgram:
return this._lintDefProgram(definition);
case N_DefInteractiveProgram:
return this._lintDefInteractiveProgram(definition);
case N_DefProcedure:
return this._lintDefProcedure(definition);
case N_DefFunction:
return this._lintDefFunction(definition);
case N_DefType:
return this._lintDefType();
default:
throw Error('Linter: Definition not implemented: ' + Symbol.keyFor(definition.tag));
}
}
public _lintDefProgram(definition: { tag?: symbol; body?: any }): void {
/* Lint body */
this._lintStmtBlock(definition.body, true /* allowReturn */);
/* Remove all local names */
this._symtable.exitScope();
}
public _lintDefInteractiveProgram(definition: { tag?: symbol; branches?: any }): void {
/* Lint all branches */
this._lintSwitchBranches(definition.branches, true /* isInteractiveProgram */);
}
public _lintDefProcedure(definition: {
tag?: symbol;
body?: any;
startPos?: any;
endPos?: any;
name?: any;
parameters?: any;
}): void {
/* Check that it does not have a return statement */
if (isBlockWithReturn(definition.body)) {
this._lintCheck(
definition.startPos,
definition.endPos,
'procedure-should-not-have-return',
[definition.name.value]
);
}
/* Add parameters as local names */
for (const parameter of definition.parameters) {
this._symtable.addNewLocalName(parameter, LocalParameter);
}
/* Lint body */
this._lintStmtBlock(definition.body, false /* !allowReturn */);
/* Remove all local names */
this._symtable.exitScope();
}
public _lintDefFunction(definition: {
tag?: symbol;
body?: any;
startPos?: any;
endPos?: any;
name?: any;
parameters?: any;
}): void {
/* Check that it has a return statement */
if (!isBlockWithReturn(definition.body)) {
this._lintCheck(definition.startPos, definition.endPos, 'function-should-have-return', [
definition.name.value
]);
}
/* Add parameters as local names */
for (const parameter of definition.parameters) {
this._symtable.addNewLocalName(parameter, LocalParameter);
}
/* Lint body */
this._lintStmtBlock(definition.body, true /* allowReturn */);
/* Remove all local names */
this._symtable.exitScope();
}
public _lintDefType(): void {
/* No restrictions */
}
/** Statements **/
public _lintStatement(statement: { tag: symbol }): void {
switch (statement.tag) {
case N_StmtBlock:
/* Do not allow return in nested blocks */
return this._lintStmtBlock(statement, false /* !allowReturn */);
case N_StmtReturn:
return this._lintStmtReturn(statement);
case N_StmtIf:
return this._lintStmtIf(statement);
case N_StmtRepeat:
return this._lintStmtRepeat(statement);
case N_StmtForeach:
return this._lintStmtForeach(statement);
case N_StmtWhile:
return this._lintStmtWhile(statement);
case N_StmtSwitch:
return this._lintStmtSwitch(statement);
case N_StmtAssignVariable:
return this._lintStmtAssignVariable(statement);
case N_StmtAssignTuple:
return this._lintStmtAssignTuple(statement);
case N_StmtProcedureCall:
return this._lintStmtProcedureCall(statement);
default:
throw Error('Linter: Statement not implemented: ' + Symbol.keyFor(statement.tag));
}
}
public _lintStmtBlock(block: { tag?: symbol; statements?: any }, allowReturn: boolean): void {
let i = 0;
for (const statement of block.statements) {
const returnAllowed = allowReturn && i === block.statements.length - 1;
if (!returnAllowed && statement.tag === N_StmtReturn) {
this._lintCheck(
statement.startPos,
statement.endPos,
'return-statement-not-allowed-here',
[]
);
}
this._lintStatement(statement);
i++;
}
}
public _lintStmtReturn(statement: { tag?: symbol; result?: any }): void {
this._lintExpression(statement.result);
}
public _lintStmtIf(statement: {
tag?: symbol;
condition?: any;
thenBlock?: any;
elseBlock?: any;
}): void {
this._lintExpression(statement.condition);
this._lintStatement(statement.thenBlock);
if (statement.elseBlock !== undefined) {
this._lintStatement(statement.elseBlock);
}
}
public _lintStmtRepeat(statement: { tag?: symbol; times?: any; body?: any }): void {
this._lintExpression(statement.times);
this._lintStatement(statement.body);
}
public _lintStmtForeach(statement: {
tag?: symbol;
pattern?: any;
range?: any;
body?: any;
}): void {
this._lintStmtForeachPattern(statement.pattern);
this._lintExpression(statement.range);
for (const variable of statement.pattern.boundVariables) {
this._symtable.addNewLocalName(variable, LocalIndex);
}
this._lintStatement(statement.body);
for (const variable of statement.pattern.boundVariables) {
this._symtable.removeLocalName(variable);
}
}
public _lintStmtForeachPattern(pattern: {
tag?: any;
startPos: any;
endPos: any;
constructorName: any;
boundVariables: any;
}): void {
/* If "DestructuringForeach" is disabled, forbid complex patterns.
* Allow only variable patterns (indices). */
if (pattern.tag !== N_PatternVariable) {
this._lintCheck(
pattern.startPos,
pattern.endPos,
'forbidden-extension-destructuring-foreach',
[]
);
}
/* Check that the pattern itself is well-formed */
this._lintPattern(pattern);
/* The pattern in a foreach cannot be an event */
const patternType = this._patternType(pattern);
if (patternType === i18n('TYPE:Event')) {
this._lintCheck(
pattern.startPos,
pattern.endPos,
'patterns-in-foreach-must-not-be-events',
[]
);
}
}
public _lintStmtWhile(statement: { tag?: symbol; condition?: any; body?: any }): void {
this._lintExpression(statement.condition);
this._lintStatement(statement.body);
}
public _lintStmtSwitch(statement: { tag?: symbol; subject?: any; branches?: any }): void {
this._lintExpression(statement.subject);
this._lintSwitchBranches(statement.branches, false /* !isInteractiveProgram */);
}
public _lintSwitchBranches(branches: any[], isInteractiveProgram: boolean): void {
this._lintBranches(branches, isInteractiveProgram, false /* isMatching */);
}
public _lintBranches(
branches: any[],
isInteractiveProgram: boolean,
isMatching: boolean
): void {
/* Check that each pattern is well-formed */
for (const branch of branches) {
this._lintPattern(branch.pattern);
}
this._branchesCheckWildcardAndVariable(branches);
this._branchesCheckNoRepeats(branches);
this._branchesCheckCompatible(branches);
if (isInteractiveProgram) {
this._branchesCheckTypeEvent(branches);
} else {
this._branchesCheckTypeNotEvent(branches);
}
/* Lint recursively each branch */
for (const branch of branches) {
this._lintBranchBody(branch, isMatching);
}
}
/* Check that there is at most one wildcard/variable pattern at the end */
public _branchesCheckWildcardAndVariable(branches: any[]): void {
let i = 0;
const n = branches.length;
for (const branch of branches) {
if (branch.pattern.tag === N_PatternWildcard && i !== n - 1) {
this._lintCheck(
branch.pattern.startPos,
branch.pattern.endPos,
'wildcard-pattern-should-be-last',
[]
);
}
if (branch.pattern.tag === N_PatternVariable && i !== n - 1) {
this._lintCheck(
branch.pattern.startPos,
branch.pattern.endPos,
'variable-pattern-should-be-last',
[branch.pattern.variableName.value]
);
}
i++;
}
}
/* Check that there are no repeated constructors in a sequence
* of branches. */
public _branchesCheckNoRepeats(branches: any[]): void {
const coveredNumbers = {};
const coveredConstructors = {};
const coveredTuples = {};
let coveredTimeout = false;
for (const branch of branches) {
switch (branch.pattern.tag) {
case N_PatternWildcard:
case N_PatternVariable:
/* Already checked in _switchBranchesCheckWildcardAndVariable */
break;
case N_PatternNumber: {
const number = branch.pattern.number.value;
if (number in coveredNumbers) {
this._lintCheck(
branch.pattern.startPos,
branch.pattern.endPos,
'numeric-pattern-repeats-number',
[number]
);
}
coveredNumbers[number] = true;
break;
}
case N_PatternStructure: {
const constructorName = branch.pattern.constructorName.value;
if (constructorName in coveredConstructors) {
this._lintCheck(
branch.pattern.startPos,
branch.pattern.endPos,
'structure-pattern-repeats-constructor',
[constructorName]
);
}
coveredConstructors[constructorName] = true;
break;
}
case N_PatternTuple: {
const arity = branch.pattern.boundVariables.length;
if (arity in coveredTuples) {
this._lintCheck(
branch.pattern.startPos,
branch.pattern.endPos,
'structure-pattern-repeats-tuple-arity',
[arity]
);
}
coveredTuples[arity] = true;
break;
}
case N_PatternTimeout: {
if (coveredTimeout) {
this._lintCheck(
branch.pattern.startPos,
branch.pattern.endPos,
'structure-pattern-repeats-timeout',
[]
);
}
coveredTimeout = true;
break;
}
default:
throw Error(
'Linter: pattern "' +
Symbol.keyFor(branch.pattern.tag) +
'" not implemented.'
);
}
}
}
/* Check that constructors are compatible,
* i.e. that they belong to the same type */
public _branchesCheckCompatible(branches: any[]): void {
let expectedType;
for (const branch of branches) {
const patternType = this._patternType(branch.pattern);
if (expectedType === undefined) {
expectedType = patternType;
} else if (patternType !== undefined && expectedType !== patternType) {
this._lintCheck(
branch.pattern.startPos,
branch.pattern.endPos,
'pattern-does-not-match-type',
[i18nF('<pattern-type>')(expectedType), i18nF('<pattern-type>')(patternType)]
);
}
}
}
/* Check that there are patterns are of type Event */
public _branchesCheckTypeEvent(branches: any[]): void {
for (const branch of branches) {
const patternType = this._patternType(branch.pattern);
if (patternType !== undefined && patternType !== i18n('TYPE:Event')) {
this._lintCheck(
branch.pattern.startPos,
branch.pattern.endPos,
'patterns-in-interactive-program-must-be-events',
[]
);
}
if (branch.pattern.tag === N_PatternVariable) {
this._lintCheck(
branch.pattern.startPos,
branch.pattern.endPos,
'patterns-in-interactive-program-cannot-be-variables',
[]
);
}
}
}
/* Check that there are no patterns of type Event */
public _branchesCheckTypeNotEvent(branches: any[]): void {
for (const branch of branches) {
const patternType = this._patternType(branch.pattern);
if (patternType === i18n('TYPE:Event')) {
this._lintCheck(
branch.pattern.startPos,
branch.pattern.endPos,
'patterns-in-switch-must-not-be-events',
[]
);
}
}
}
/* Recursively lint the body of each branch. Locally bind variables. */
public _lintBranchBody(
branch: { pattern: { boundVariables: any }; body: { tag: symbol } },
isMatching: boolean
): void {
for (const variable of branch.pattern.boundVariables) {
this._symtable.addNewLocalName(variable, LocalParameter);
}
if (isMatching) {
this._lintExpression(branch.body);
} else {
this._lintStatement(branch.body);
}
for (const variable of branch.pattern.boundVariables) {
this._symtable.removeLocalName(variable);
}
}
/* Return a description of the type of a pattern */
public _patternType(pattern: {
tag?: any;
startPos: any;
endPos: any;
constructorName: any;
boundVariables: any;
}): string {
switch (pattern.tag) {
case N_PatternWildcard:
case N_PatternVariable:
return undefined;
case N_PatternNumber:
return toStr(i18n('TYPE:Integer'));
case N_PatternStructure:
return this._symtable.constructorType(pattern.constructorName.value);
case N_PatternTuple:
return '_TUPLE_' + pattern.boundVariables.length.toString();
case N_PatternTimeout:
return toStr(i18n('TYPE:Event'));
default:
throw Error(
'Linter: pattern "' + Symbol.keyFor(pattern.tag) + '" not implemented.'
);
}
}
public _lintStmtAssignVariable(statement: { tag?: symbol; variable?: any; value?: any }): void {
this._symtable.setLocalName(statement.variable, LocalVariable);
this._lintExpression(statement.value);
}
public _lintStmtAssignTuple(statement: { tag?: symbol; variables?: any; value?: any }): void {
const variables = {};
for (const variable of statement.variables) {
this._symtable.setLocalName(variable, LocalVariable);
if (variable.value in variables) {
this._lintCheck(
variable.startPos,
variable.endPos,
'repeated-variable-in-tuple-assignment',
[variable.value]
);
}
variables[variable.value] = true;
}
this._lintExpression(statement.value);
}
public _lintStmtProcedureCall(statement: {
tag?: symbol;
procedureName?: any;
startPos?: any;
endPos?: any;
args?: any;
}): void {
const name = statement.procedureName.value;
/* Check that it is a procedure */
if (!this._symtable.isProcedure(name)) {
if (this._symtable.isConstructor(name)) {
this._lintCheck(
statement.startPos,
statement.endPos,
'constructor-used-as-procedure',
[name, this._symtable.constructorType(name)]
);
} else {
this._lintCheck(statement.startPos, statement.endPos, 'undefined-procedure', [
name
]);
}
}
/* Check that the number of argument coincides */
const expected = this._symtable.procedureParameters(name).length;
const received = statement.args.length;
if (expected !== received) {
this._lintCheck(statement.startPos, statement.endPos, 'procedure-arity-mismatch', [
name,
expected,
received
]);
}
/* Check all the arguments */
for (const argument of statement.args) {
this._lintExpression(argument);
}
}
/** Patterns **/
public _lintPattern(pattern: {
tag?: any;
startPos: any;
endPos: any;
constructorName: any;
boundVariables: any;
}): void {
switch (pattern.tag) {
case N_PatternWildcard:
return this._lintPatternWildcard();
case N_PatternVariable:
return this._lintPatternVariable();
case N_PatternNumber:
return this._lintPatternNumber();
case N_PatternStructure:
return this._lintPatternStructure(pattern);
case N_PatternTuple:
return this._lintPatternTuple();
case N_PatternTimeout:
return this._lintPatternTimeout();
default:
throw Error(
'Linter: pattern "' + Symbol.keyFor(pattern.tag) + '" not implemented.'
);
}
}
public _lintPatternWildcard(): void {
/* No restrictions */
}
public _lintPatternVariable(): void {
/* No restrictions */
}
public _lintPatternNumber(): void {
/* No restrictions */
}
public _lintPatternStructure(pattern: {
tag?: any;
startPos: any;
endPos: any;
constructorName: any;
boundVariables: any;
}): void {
const name = pattern.constructorName.value;
/* Check that the constructor exists */
if (!this._symtable.isConstructor(name)) {
this._failExpectedConstructorButGot(
// throws
pattern.startPos,
pattern.endPos,
name
);
return;
}
/* Check that the number of parameters match.
* Note: constructor patterns with 0 arguments are always allowed.
*/
const expected = this._symtable.constructorFields(name).length;
const received = pattern.boundVariables.length;
if (received > 0 && expected !== received) {
this._lintCheck(pattern.startPos, pattern.endPos, 'structure-pattern-arity-mismatch', [
name,
expected,
received
]);
}
}
public _lintPatternTuple(): void {
/* No restrictions */
}
public _lintPatternTimeout(): void {
/* No restrictions */
}
/** Expressions **/
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
public _lintExpression(expression: any): void {
switch (expression.tag) {
case N_ExprVariable:
return this._lintExprVariable();
case N_ExprConstantNumber:
return this._lintExprConstantNumber();
case N_ExprConstantString:
return this._lintExprConstantString();
case N_ExprChoose:
return this._lintExprChoose(expression);
case N_ExprMatching:
return this._lintExprMatching(expression);
case N_ExprList:
return this._lintExprList(expression);
case N_ExprRange:
return this._lintExprRange(expression);
case N_ExprTuple:
return this._lintExprTuple(expression);
case N_ExprStructure:
return this._lintExprStructure(expression);
case N_ExprStructureUpdate:
return this._lintExprStructureUpdate(expression);
case N_ExprFunctionCall:
return this._lintExprFunctionCall(expression);
default:
throw Error('Linter: Expression not implemented: ' + Symbol.keyFor(expression.tag));
}
}
public _lintExprVariable(): void {
/* No restrictions.
* Note: the restriction that a variable is defined before it is used
* is a dynamic constraint . */
}
public _lintExprConstantNumber(): void {
/* No restrictions */
}
public _lintExprConstantString(): void {
/* No restrictions */
}
public _lintExprChoose(expression: { condition: any; trueExpr: any; falseExpr: any }): void {
this._lintExpression(expression.condition);
this._lintExpression(expression.trueExpr);
this._lintExpression(expression.falseExpr);
}
public _lintExprMatching(expression: { subject: any; branches: any }): void {
this._lintExpression(expression.subject);
this._lintMatchingBranches(expression.branches);
}
public _lintMatchingBranches(branches: any[]): void {
this._lintBranches(branches, false /* !isInteractiveProgram */, true /* isMatching */);
}
public _lintExprList(expression: { elements: any }): void {
for (const element of expression.elements) {
this._lintExpression(element);
}
}
public _lintExprRange(expression: { first: any; second: any; last: any }): void {
this._lintExpression(expression.first);
if (expression.second !== undefined) {
this._lintExpression(expression.second);
}
this._lintExpression(expression.last);
}
public _lintExprTuple(expression: { elements: any }): void {
for (const element of expression.elements) {
this._lintExpression(element);
}
}
public _lintExprStructure(expression: {
tag?: symbol;
constructorName?: any;
startPos: any;
endPos: any;
fieldBindings?: any;
original?: any;
fieldNames?: (() => any) | (() => any) | (() => any);
}): void {
this._lintExprStructureOrUpdate(expression, undefined);
}
public _lintExprStructureUpdate(expression: {
tag?: symbol;
constructorName?: any;
startPos: any;
endPos: any;
fieldBindings?: any;
original?: any;
fieldNames?: (() => any) | (() => any) | (() => any);
}): void {
this._lintExprStructureOrUpdate(expression, expression.original);
}
/* Check a structure construction: C(x1 <- e1, ..., xN <- eN)
* or a structure update: C(original | x1 <- e1, ..., xN <- eN).
*
* If original is undefined, it is a structure construction.
* If original is not undefined, it is the updated expression.
* */
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
public _lintExprStructureOrUpdate(expression: any, original: { tag: symbol }): void {
/* Check that constructor exists */
const constructorName: string = expression.constructorName.value;
if (!this._symtable.isConstructor(constructorName)) {
this._failExpectedConstructorButGot(
// throws
expression.startPos,
expression.endPos,
constructorName
);
return;
}
this._checkStructureTypeNotEvent(constructorName, expression);
this._checkStructureNoRepeatedFields(constructorName, expression);
this._checkStructureBindingsCorrect(constructorName, expression);
/* If it is a structure construction, check that the fields are complete */
if (original === undefined) {
this._checkStructureBindingsComplete(constructorName, expression);
}
/* If it is an update, recursively check the original expression */
if (original !== undefined) {
this._lintExpression(original);
}
/* Recursively check expressions in field bindings */
for (const fieldBinding of expression.fieldBindings) {
this._lintExpression(fieldBinding.value);
}
}
/* Check that there are no repeated fields in a structure
* construction/update */
public _checkStructureNoRepeatedFields(
constructorName: string,
expression: { fieldNames: () => any; startPos: SourceReader; endPos: SourceReader }
): void {
const declaredFields = expression.fieldNames();
const seen = {};
for (const fieldName of declaredFields) {
if (fieldName in seen) {
this._lintCheck(
expression.startPos,
expression.endPos,
'structure-construction-repeated-field',
[constructorName, fieldName]
);
}
seen[fieldName] = true;
}
}
/* Check that all bindings in a structure construction/update
* correspond to existing fields */
public _checkStructureBindingsCorrect(
constructorName: string,
expression: { fieldNames: () => any; startPos: SourceReader; endPos: SourceReader }
): void {
const declaredFields = expression.fieldNames();
const constructorFields = this._symtable.constructorFields(constructorName);
for (const fieldName of declaredFields) {
if (constructorFields.indexOf(fieldName) === -1) {
this._lintCheck(
expression.startPos,
expression.endPos,
'structure-construction-invalid-field',
[constructorName, fieldName]
);
}
}
}
/* Check that bindings in a structure construction/update
* cover all existing fields */
public _checkStructureBindingsComplete(
constructorName: string,
expression: { fieldNames: () => any; startPos: SourceReader; endPos: SourceReader }
): void {
const declaredFields = expression.fieldNames();
const constructorFields = this._symtable.constructorFields(constructorName);
for (const fieldName of constructorFields) {
if (declaredFields.indexOf(fieldName) === -1) {
this._lintCheck(
expression.startPos,
expression.endPos,
'structure-construction-missing-field',
[constructorName, fieldName]
);
}
}
}
/* Check that a structure construction/update does not involve
* constructors of the Event type, which should only be
* handled implicitly in an interactive program. */
public _checkStructureTypeNotEvent(
constructorName: string,
expression: { startPos: SourceReader; endPos: SourceReader }
): void {
const constructorType = this._symtable.constructorType(constructorName);
if (constructorType === i18n('TYPE:Event')) {
this._lintCheck(
expression.startPos,
expression.endPos,
'structure-construction-cannot-be-an-event',
[constructorName]
);
}
}
public _lintExprFunctionCall(expression: {
functionName: { value: any };
startPos: SourceReader;
endPos: SourceReader;
args: string | any[];
}): void {
/* Check that it is a function or a field */
const name = expression.functionName.value;
if (!this._symtable.isFunction(name) && !this._symtable.isField(name)) {
this._lintCheck(expression.startPos, expression.endPos, 'undefined-function', [name]);
}
/* Check that the number of argument coincides */
let expected: number;
if (this._symtable.isFunction(name)) {
expected = this._symtable.functionParameters(name).length;
} else {
/* Fields always have exactly one parameter */
expected = 1;
}
const received = expression.args.length;
if (expected !== received) {
this._lintCheck(expression.startPos, expression.endPos, 'function-arity-mismatch', [
name,
expected,
received
]);
}
/* Recursively check arguments */
for (const argument of expression.args) {
this._lintExpression(argument);
}
}
public _disableRecursion(ast: ASTMain): void {
if (this._enabledLinterChecks['forbidden-extension-allow-recursion']) {
const cycle = new RecursionChecker().callCycle(ast);
if (cycle !== undefined) {
this._lintCheck(
cycle[0].location.startPos,
cycle[0].location.endPos,
'forbidden-extension-allow-recursion',
[cycle]
);
}
}
}
/* Throw a syntax error indicating that we expected the name of a
* constructor, but we got a name which is not a constructor.
*
* If the name is a type or a procedure, provide a more helpful
* error message. (Coinciding constructor and procedure names are
* not forbidden, but it is probably a mistake). */
public _failExpectedConstructorButGot(
startPos: SourceReader,
endPos: SourceReader,
name: string
): void {
if (this._symtable.isType(name)) {
this._lintCheck(startPos, endPos, 'type-used-as-constructor', [
name,
this._symtable.typeConstructors(name)
]);
} else if (this._symtable.isProcedure(name)) {
this._lintCheck(startPos, endPos, 'procedure-used-as-constructor', [name]);
} else {
this._lintCheck(startPos, endPos, 'undeclared-constructor', [name]);
}
}
}