Files
privacy.sexy/src/application/Parser/Script/Compiler/Function/SharedFunctionsParser.ts
undergroundwires 5bbbb9cecc Refactor to remove code coupling with Webpack
Remove using Webpack import syntax such as: `js-yaml-loader!@/..`. It's
a non-standard syntax that couples the code to Webpack.

Configure instead by specifying Webpack loader in Vue configuration
file.

Enable related ESLint rules.

Remove unused dependency `raw-loader` and refactor
`NoUnintendedInlining` test to load files using file system (dropping
webpack dependency).

Refactor to use `import type` for type imports to show the indent
clearly and satisfy failing ESLint rules.
2022-01-31 17:22:34 +01:00

144 lines
5.1 KiB
TypeScript

import type { FunctionData, InstructionHolder } from '@/application/collections/';
import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction';
import { SharedFunctionCollection } from './SharedFunctionCollection';
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
import { ISharedFunctionsParser } from './ISharedFunctionsParser';
import { FunctionParameter } from './Parameter/FunctionParameter';
import { FunctionParameterCollection } from './Parameter/FunctionParameterCollection';
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
import { ISharedFunction } from './ISharedFunction';
import { parseFunctionCalls } from './Call/FunctionCallParser';
export class SharedFunctionsParser implements ISharedFunctionsParser {
public static readonly instance: ISharedFunctionsParser = new SharedFunctionsParser();
public parseFunctions(
functions: readonly FunctionData[],
): ISharedFunctionCollection {
const collection = new SharedFunctionCollection();
if (!functions || !functions.length) {
return collection;
}
ensureValidFunctions(functions);
return functions
.map((func) => parseFunction(func))
.reduce((acc, func) => {
acc.addFunction(func);
return acc;
}, collection);
}
}
function parseFunction(data: FunctionData): ISharedFunction {
const { name } = data;
const parameters = parseParameters(data);
if (hasCode(data)) {
return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode);
}
// Has call
const calls = parseFunctionCalls(data.call);
return createCallerFunction(name, parameters, calls);
}
function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection {
return (data.parameters || [])
.map((parameter) => {
try {
return new FunctionParameter(
parameter.name,
parameter.optional || false,
);
} catch (err) {
throw new Error(`"${data.name}": ${err.message}`);
}
})
.reduce((parameters, parameter) => {
parameters.addParameter(parameter);
return parameters;
}, new FunctionParameterCollection());
}
function hasCode(data: FunctionData): boolean {
return Boolean(data.code);
}
function hasCall(data: FunctionData): boolean {
return Boolean(data.call);
}
function ensureValidFunctions(functions: readonly FunctionData[]) {
ensureNoUndefinedItem(functions);
ensureNoDuplicatesInFunctionNames(functions);
ensureNoDuplicateCode(functions);
ensureEitherCallOrCodeIsDefined(functions);
ensureExpectedParametersType(functions);
}
function printList(list: readonly string[]): string {
return `"${list.join('","')}"`;
}
function ensureEitherCallOrCodeIsDefined(holders: readonly InstructionHolder[]) {
// Ensure functions do not define both call and code
const withBothCallAndCode = holders.filter((holder) => hasCode(holder) && hasCall(holder));
if (withBothCallAndCode.length) {
throw new Error(`both "code" and "call" are defined in ${printNames(withBothCallAndCode)}`);
}
// Ensure functions have either code or call
const hasEitherCodeOrCall = holders.filter((holder) => !hasCode(holder) && !hasCall(holder));
if (hasEitherCodeOrCall.length) {
throw new Error(`neither "code" or "call" is defined in ${printNames(hasEitherCodeOrCall)}`);
}
}
function ensureExpectedParametersType(functions: readonly FunctionData[]) {
const unexpectedFunctions = functions
.filter((func) => func.parameters && !isArrayOfObjects(func.parameters));
if (unexpectedFunctions.length) {
const errorMessage = `parameters must be an array of objects in function(s) ${printNames(unexpectedFunctions)}`;
throw new Error(errorMessage);
}
}
function isArrayOfObjects(value: unknown): boolean {
return Array.isArray(value)
&& value.every((item) => typeof item === 'object');
}
function printNames(holders: readonly InstructionHolder[]) {
return printList(holders.map((holder) => holder.name));
}
function ensureNoDuplicatesInFunctionNames(functions: readonly FunctionData[]) {
const duplicateFunctionNames = getDuplicates(functions
.map((func) => func.name.toLowerCase()));
if (duplicateFunctionNames.length) {
throw new Error(`duplicate function name: ${printList(duplicateFunctionNames)}`);
}
}
function ensureNoUndefinedItem(functions: readonly FunctionData[]) {
if (functions.some((func) => !func)) {
throw new Error('some functions are undefined');
}
}
function ensureNoDuplicateCode(functions: readonly FunctionData[]) {
const duplicateCodes = getDuplicates(functions
.map((func) => func.code)
.filter((code) => code));
if (duplicateCodes.length > 0) {
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
}
const duplicateRevertCodes = getDuplicates(functions
.filter((func) => func.revertCode)
.map((func) => func.revertCode));
if (duplicateRevertCodes.length > 0) {
throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`);
}
}
function getDuplicates(texts: readonly string[]): string[] {
return texts.filter((item, index) => texts.indexOf(item) !== index);
}