Relax and improve code validation
Rework code validation to be bound to a context and not context-independent. It means that the generated code is validated based on different phases during the compilation. This is done by moving validation from `ScriptCode` constructor to a different callable function. It removes duplicate detection for function calls once a call is fully compiled, but still checks for duplicates inside each function body that has inline code. This allows for having duplicates in final scripts (thus relaxing the duplicate detection), e.g., when multiple calls to the same function is made. It fixes non-duplicates (when using common syntax) being misrepresented as duplicate lines. It improves the output of errors, such as printing valid lines, to give more context. This improvement also fixes empty line validation not showing the right empty lines in the error output. Empty line validation shows tabs and whitespaces more clearly. Finally, it adds more tests including tests for existing logic, such as singleton factories.
This commit is contained in:
@@ -4,25 +4,18 @@ export class ScriptCode implements IScriptCode {
|
||||
constructor(
|
||||
public readonly execute: string,
|
||||
public readonly revert: string,
|
||||
syntax: ILanguageSyntax,
|
||||
) {
|
||||
if (!syntax) { throw new Error('missing syntax'); }
|
||||
validateCode(execute, syntax);
|
||||
validateRevertCode(revert, execute, syntax);
|
||||
validateCode(execute);
|
||||
validateRevertCode(revert, execute);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ILanguageSyntax {
|
||||
readonly commentDelimiters: string[];
|
||||
readonly commonCodeParts: string[];
|
||||
}
|
||||
|
||||
function validateRevertCode(revertCode: string, execute: string, syntax: ILanguageSyntax) {
|
||||
function validateRevertCode(revertCode: string, execute: string) {
|
||||
if (!revertCode) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
validateCode(revertCode, syntax);
|
||||
validateCode(revertCode);
|
||||
if (execute === revertCode) {
|
||||
throw new Error('Code itself and its reverting code cannot be the same');
|
||||
}
|
||||
@@ -31,54 +24,8 @@ function validateRevertCode(revertCode: string, execute: string, syntax: ILangua
|
||||
}
|
||||
}
|
||||
|
||||
function validateCode(code: string, syntax: ILanguageSyntax): void {
|
||||
function validateCode(code: string): void {
|
||||
if (!code || code.length === 0) {
|
||||
throw new Error('missing code');
|
||||
}
|
||||
ensureNoEmptyLines(code);
|
||||
ensureCodeHasUniqueLines(code, syntax);
|
||||
}
|
||||
|
||||
function ensureNoEmptyLines(code: string): void {
|
||||
const lines = code.split(/\r\n|\r|\n/);
|
||||
if (lines.some((line) => line.trim().length === 0)) {
|
||||
throw Error(`Script has empty lines:\n${lines.map((part, index) => `\n (${index}) ${part || '❌'}`).join('')}`);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureCodeHasUniqueLines(code: string, syntax: ILanguageSyntax): void {
|
||||
const allLines = code.split(/\r\n|\r|\n/);
|
||||
const checkedLines = allLines.filter((line) => !shouldIgnoreLine(line, syntax));
|
||||
if (checkedLines.length === 0) {
|
||||
return;
|
||||
}
|
||||
const duplicateLines = checkedLines.filter((e, i, a) => a.indexOf(e) !== i);
|
||||
if (duplicateLines.length !== 0) {
|
||||
throw Error(`Duplicates detected in script:\n${printDuplicatedLines(allLines)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function printDuplicatedLines(allLines: string[]) {
|
||||
return allLines
|
||||
.map((line, index) => {
|
||||
const occurrenceIndices = allLines
|
||||
.map((e, i) => (e === line ? i : ''))
|
||||
.filter(String);
|
||||
const isDuplicate = occurrenceIndices.length > 1;
|
||||
const indicator = isDuplicate ? `❌ (${occurrenceIndices.join(',')})\t` : '✅ ';
|
||||
return `${indicator}[${index}] ${line}`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function shouldIgnoreLine(codeLine: string, syntax: ILanguageSyntax): boolean {
|
||||
const lowerCaseCodeLine = codeLine.toLowerCase();
|
||||
const isCommentLine = () => syntax.commentDelimiters.some(
|
||||
(delimiter) => lowerCaseCodeLine.startsWith(delimiter),
|
||||
);
|
||||
const consistsOfFrequentCommands = () => {
|
||||
const trimmed = lowerCaseCodeLine.trim().split(' ');
|
||||
return trimmed.every((part) => syntax.commonCodeParts.includes(part));
|
||||
};
|
||||
return isCommentLine() || consistsOfFrequentCommands();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user