Refactor to unify scripts/categories as Executable
This commit consolidates scripts and categories under a unified 'Executable' concept. This simplifies the architecture and improves code readability. - Introduce subfolders within `src/domain` to segregate domain elements. - Update class and interface names by removing the 'I' prefix in alignment with new coding standards. - Replace 'Node' with 'Executable' to clarify usage; reserve 'Node' exclusively for the UI's tree component.
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import type { ICodeLine } from './ICodeLine';
|
||||
import type { ICodeValidationRule, IInvalidCodeLine } from './ICodeValidationRule';
|
||||
import type { ICodeValidator } from './ICodeValidator';
|
||||
|
||||
export class CodeValidator implements ICodeValidator {
|
||||
public static readonly instance: ICodeValidator = new CodeValidator();
|
||||
|
||||
public throwIfInvalid(
|
||||
code: string,
|
||||
rules: readonly ICodeValidationRule[],
|
||||
): void {
|
||||
if (rules.length === 0) { throw new Error('missing rules'); }
|
||||
if (!code) {
|
||||
return;
|
||||
}
|
||||
const lines = extractLines(code);
|
||||
const invalidLines = rules.flatMap((rule) => rule.analyze(lines));
|
||||
if (invalidLines.length === 0) {
|
||||
return;
|
||||
}
|
||||
const errorText = `Errors with the code.\n${printLines(lines, invalidLines)}`;
|
||||
throw new Error(errorText);
|
||||
}
|
||||
}
|
||||
|
||||
function extractLines(code: string): ICodeLine[] {
|
||||
return code
|
||||
.split(/\r\n|\r|\n/)
|
||||
.map((lineText, lineIndex): ICodeLine => ({
|
||||
index: lineIndex + 1,
|
||||
text: lineText,
|
||||
}));
|
||||
}
|
||||
|
||||
function printLines(
|
||||
lines: readonly ICodeLine[],
|
||||
invalidLines: readonly IInvalidCodeLine[],
|
||||
): string {
|
||||
return lines.map((line) => {
|
||||
const badLine = invalidLines.find((invalidLine) => invalidLine.index === line.index);
|
||||
if (!badLine) {
|
||||
return `[${line.index}] ✅ ${line.text}`;
|
||||
}
|
||||
return `[${badLine.index}] ❌ ${line.text}\n\t⟶ ${badLine.error}`;
|
||||
}).join('\n');
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface ICodeLine {
|
||||
readonly index: number;
|
||||
readonly text: string;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { ICodeLine } from './ICodeLine';
|
||||
|
||||
export interface IInvalidCodeLine {
|
||||
readonly index: number;
|
||||
readonly error: string;
|
||||
}
|
||||
|
||||
export interface ICodeValidationRule {
|
||||
analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[];
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { ICodeValidationRule } from './ICodeValidationRule';
|
||||
|
||||
export interface ICodeValidator {
|
||||
throwIfInvalid(
|
||||
code: string,
|
||||
rules: readonly ICodeValidationRule[],
|
||||
): void;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
|
||||
import type { ICodeLine } from '../ICodeLine';
|
||||
import type { ICodeValidationRule, IInvalidCodeLine } from '../ICodeValidationRule';
|
||||
|
||||
export class NoDuplicatedLines implements ICodeValidationRule {
|
||||
constructor(private readonly syntax: ILanguageSyntax) { }
|
||||
|
||||
public analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[] {
|
||||
return lines
|
||||
.map((line): IDuplicateAnalyzedLine => ({
|
||||
index: line.index,
|
||||
isIgnored: shouldIgnoreLine(line.text, this.syntax),
|
||||
occurrenceIndices: lines
|
||||
.filter((other) => other.text === line.text)
|
||||
.map((duplicatedLine) => duplicatedLine.index),
|
||||
}))
|
||||
.filter((line) => hasInvalidDuplicates(line))
|
||||
.map((line): IInvalidCodeLine => ({
|
||||
index: line.index,
|
||||
error: `Line is duplicated at line numbers ${line.occurrenceIndices.join(',')}.`,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
interface IDuplicateAnalyzedLine {
|
||||
readonly index: number;
|
||||
readonly occurrenceIndices: readonly number[];
|
||||
readonly isIgnored: boolean;
|
||||
}
|
||||
|
||||
function hasInvalidDuplicates(line: IDuplicateAnalyzedLine): boolean {
|
||||
return !line.isIgnored && line.occurrenceIndices.length > 1;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { ICodeValidationRule, IInvalidCodeLine } from '../ICodeValidationRule';
|
||||
import type { ICodeLine } from '../ICodeLine';
|
||||
|
||||
export class NoEmptyLines implements ICodeValidationRule {
|
||||
public analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[] {
|
||||
return lines
|
||||
.filter((line) => (line.text?.trim().length ?? 0) === 0)
|
||||
.map((line): IInvalidCodeLine => ({
|
||||
index: line.index,
|
||||
error: (() => {
|
||||
if (!line.text) {
|
||||
return 'Empty line';
|
||||
}
|
||||
const markedText = line.text
|
||||
.replaceAll(' ', '{whitespace}')
|
||||
.replaceAll('\t', '{tab}');
|
||||
return `Empty line: "${markedText}"`;
|
||||
})(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
|
||||
|
||||
const BatchFileCommonCodeParts = ['(', ')', 'else', '||'];
|
||||
const PowerShellCommonCodeParts = ['{', '}'];
|
||||
|
||||
export class BatchFileSyntax implements ILanguageSyntax {
|
||||
public readonly commentDelimiters = ['REM', '::'];
|
||||
|
||||
public readonly commonCodeParts = [...BatchFileCommonCodeParts, ...PowerShellCommonCodeParts];
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface ILanguageSyntax {
|
||||
readonly commentDelimiters: string[];
|
||||
readonly commonCodeParts: string[];
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import type { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
|
||||
import type { ILanguageSyntax } from './ILanguageSyntax';
|
||||
|
||||
export type ISyntaxFactory = IScriptingLanguageFactory<ILanguageSyntax>;
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
|
||||
|
||||
export class ShellScriptSyntax implements ILanguageSyntax {
|
||||
public readonly commentDelimiters = ['#'];
|
||||
|
||||
public readonly commonCodeParts = ['(', ')', 'else', 'fi', 'done'];
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { ScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/ScriptingLanguageFactory';
|
||||
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
|
||||
import { BatchFileSyntax } from './BatchFileSyntax';
|
||||
import { ShellScriptSyntax } from './ShellScriptSyntax';
|
||||
import type { ISyntaxFactory } from './ISyntaxFactory';
|
||||
|
||||
export class SyntaxFactory
|
||||
extends ScriptingLanguageFactory<ILanguageSyntax>
|
||||
implements ISyntaxFactory {
|
||||
constructor() {
|
||||
super();
|
||||
this.registerGetter(ScriptingLanguage.batchfile, () => new BatchFileSyntax());
|
||||
this.registerGetter(ScriptingLanguage.shellscript, () => new ShellScriptSyntax());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user