-
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #534 from GuillaumeGomez/ast
Parse the whole file and store its AST instead of parsing command after command
- Loading branch information
Showing
79 changed files
with
2,099 additions
and
1,292 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; |
Oops, something went wrong.