Skip to content

Commit

Permalink
Merge pull request #534 from GuillaumeGomez/ast
Browse files Browse the repository at this point in the history
Parse the whole file and store its AST instead of parsing command after command
  • Loading branch information
GuillaumeGomez authored Aug 24, 2023
2 parents 8b1a4b5 + 0fe9a0f commit 61ac115
Show file tree
Hide file tree
Showing 79 changed files with 2,099 additions and 1,292 deletions.
197 changes: 197 additions & 0 deletions src/ast.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
const {
cleanString,
Element,
ExpressionsValidator,
IdentElement,
matchInteger,
NumberElement,
Parser,
StringElement,
VariableElement,
} = require('./parser.js');
const utils = require('./utils');

function replaceVariable(elem, variables, functionArgs, forceVariableAsString, errors) {
const makeError = (message, line) => {
errors.push({
'message': message,
'isFatal': true,
'line': line,
});
};
const variableName = elem.value;
const lineNumber = elem.line;
const startPos = elem.startPos;
const endPos = elem.endPos;
const associatedValue = utils.getVariableValue(variables, variableName, functionArgs);
if (associatedValue === null) {
const e = makeError(
`variable \`${variableName}\` not found in options nor environment`, lineNumber);
return new VariableElement(variableName, startPos, endPos, elem.fullText, lineNumber, e);
}
if (associatedValue instanceof Element) {
// Nothing to be done in here.
return associatedValue;
} else if (['number', 'string', 'boolean'].includes(typeof associatedValue)) {
if (typeof associatedValue === 'boolean') {
return new IdentElement(
associatedValue.toString(), startPos, endPos, lineNumber);
} else if (typeof associatedValue === 'number' ||
// eslint-disable-next-line no-extra-parens
(!forceVariableAsString && matchInteger(associatedValue) === true)) {
return new NumberElement(associatedValue, startPos, endPos, lineNumber);
}
return new StringElement(
associatedValue,
startPos,
endPos,
`"${cleanString(associatedValue)}"`,
lineNumber,
);
}
// this is a JSON dict and it should be parsed.
const p = new Parser(JSON.stringify(associatedValue));
p.currentLine = lineNumber;
p.parseJson();
errors.push(...p.errors);
return p.elems[0];
}

// In this function we voluntarily don't go into `block` elements as they'll be handled separately.
function replaceVariables(elem, variables, functionArgs, forceVariableAsString, errors) {
if (elem.kind === 'variable') {
return replaceVariable(elem, variables, functionArgs, forceVariableAsString, errors);
} else if (['expression', 'tuple', 'array'].includes(elem.kind)) {
elem.value = elem.value
.map(e => replaceVariables(e, variables, functionArgs, forceVariableAsString, errors))
.filter(e => e !== null);
} else if (elem.kind === 'json') {
elem.value = elem.value
.map(e => {
return {
'key': replaceVariables(e.key, variables, functionArgs, true, errors),
'value': replaceVariables(e.value, variables, functionArgs, false, errors),
};
})
.filter(e => e.value !== null);
}
return elem;
}

class CommandNode {
constructor(command, commandLine, ast, hasVariable, commandStart, text) {
this.commandName = command.toLowerCase();
this.line = commandLine;
this.ast = ast;
this.hasVariable = hasVariable;
this.commandStart = commandStart;
if (ast.length > 0) {
this.argsStart = ast[0].startPos;
this.argsEnd = ast[ast.length - 1].endPos;
} else {
this.argsStart = 0;
this.argsEnd = 0;
}
this.errors = [];
this.text = text;
}

getInferredAst(variables, functionArgs) {
// We clone the AST to not modify the original. And because it's JS, it's super annoying
// to do...
let inferred = [];
if (!this.hasVariable) {
inferred = this.ast.map(e => e.clone());
} else {
for (const elem of this.ast) {
const e = replaceVariables(
elem.clone(), variables, functionArgs, false, this.errors);
if (e !== null) {
inferred.push(e);
}
}
}
if (this.errors.length === 0) {
const validation = new ExpressionsValidator(inferred, true, this.text);
if (validation.errors.length !== 0) {
this.errors.push(...validation.errors);
}
}
return {
'ast': inferred,
'errors': this.errors,
};
}

getOriginalCommand() {
let end = this.argsEnd;
if (end === 0) {
end = this.commandStart + this.commandName.length;
while (end < this.text.length && this.text[end] !== ':') {
end += 1;
}
if (end < this.text.length) {
// To go beyond the ':'.
end += 1;
}
}
return this.text.slice(this.commandStart, end);
}

clone() {
const n = new this.constructor(
this.commandName,
this.line,
this.ast.map(e => e.clone()),
this.hasVariable,
this.commandStart,
this.text,
);
n.errors.push(...this.errors);
return n;
}
}

class AstLoader {
constructor(filePath, content = null) {
this.text = content === null ? utils.readFile(filePath) : content;
const parser = new Parser(this.text);
this.commands = [];
this.errors = [];
// eslint-disable-next-line no-constant-condition
while (true) {
const ret = parser.parseNextCommand();
if (ret.errors !== false) {
this.errors.push(...parser.errors);
if (parser.hasFatalError) {
// Only stop parsing if we encounter a fatal error.
break;
}
// The errors will be kept so it's fine to not store all commands node since we
// will abort before trying to run them. However, we still want to get all parser
// errors so we continue.
} else if (ret.finished === true) {
break;
} else {
this.commands.push(new CommandNode(
parser.command.getRaw(),
parser.commandLine,
parser.elems,
parser.hasVariable,
parser.commandStart,
this.text,
));
}
}
}

hasErrors() {
return this.errors.length > 0;
}
}

module.exports = {
'AstLoader': AstLoader,
'CommandNode': CommandNode,
'replaceVariables': replaceVariables,
};
Loading

0 comments on commit 61ac115

Please sign in to comment.