Merge branch 'issue-614' into 'milestone/7.0.0'

Resolve issue #614

See merge request nightlycommit/twing!607
This commit is contained in:
Eric MORAND 2024-04-15 20:29:12 +00:00
commit ec4a05cbbc
91 changed files with 309 additions and 484 deletions

View File

@ -48,7 +48,7 @@
"is-plain-object": "^2.0.4",
"isobject": "^3.0.1",
"levenshtein": "^1.0.5",
"locutus": "^2.0.11",
"locutus": "^2.0.31",
"luxon": "^1.19.3",
"pad": "^2.0.3",
"regex-parser": "^2.2.8",

View File

@ -8,12 +8,11 @@ export type {TwingError} from "./lib/error";
export type {TwingBaseError, TwingErrorLocation} from "./lib/error/base";
export type {TwingParsingError} from "./lib/error/parsing";
export type {TwingRuntimeError} from "./lib/error/runtime";
export type {TwingTemplateLoadingError} from "./lib/error/loader";
export {isATwingError} from "./lib/error";
export {createParsingError} from "./lib/error/parsing";
export {createRuntimeError, isARuntimeError} from "./lib/error/runtime";
export {createTemplateLoadingError, isATemplateLoadingError} from "./lib/error/loader";
export {createRuntimeError} from "./lib/error/runtime";
export {createTemplateLoadingError} from "./lib/error/loader";
// loader
export type {
@ -283,13 +282,7 @@ export type {
} from "./lib/operator";
export type {TwingOutputBuffer} from "./lib/output-buffer";
export type {TwingParser, TwingParserOptions} from "./lib/parser";
export type {TwingSandboxSecurityError} from "./lib/sandbox/security-error";
export type {TwingSandboxSecurityPolicy} from "./lib/sandbox/security-policy";
export type {TwingSandboxSecurityNotAllowedFilterError} from "./lib/sandbox/security-not-allowed-filter-error";
export type {TwingSandboxSecurityNotAllowedFunctionError} from "./lib/sandbox/security-not-allowed-function-error";
export type {TwingSandboxSecurityNotAllowedMethodError} from "./lib/sandbox/security-not-allowed-method-error";
export type {TwingSandboxSecurityNotAllowedPropertyError} from "./lib/sandbox/security-not-allowed-property-error";
export type {TwingSandboxSecurityNotAllowedTagError} from "./lib/sandbox/security-not-allowed-tag-error";
export type {TwingSource} from "./lib/source";
export type {TwingSourceMapRuntime} from "./lib/source-map-runtime";
export type {

View File

@ -22,7 +22,6 @@ import {createSourceMapRuntime} from "./source-map-runtime";
import {createSandboxSecurityPolicy, TwingSandboxSecurityPolicy} from "./sandbox/security-policy";
import {TwingTemplate} from "./template";
import {Settings as DateTimeSettings} from "luxon";
import {TwingParsingError} from "./error/parsing";
import {createLexer, TwingLexer} from "./lexer";
import {TwingCache} from "./cache";
import {createCoreExtension} from "./extension/core";
@ -290,17 +289,7 @@ export const createEnvironment = (
);
}
try {
return parser.parse(stream);
} catch (error: any) {
const source = stream.source;
if (!(error as TwingParsingError).source) {
(error as TwingParsingError).source = source.name;
}
throw error;
}
return parser.parse(stream);
},
render: (name, context, options) => {
return environment.loadTemplate(name)

View File

@ -1,21 +1,15 @@
import {TwingParsingError, parsingErrorName} from "./error/parsing";
import {TwingRuntimeError, runtimeErrorName} from "./error/runtime";
import {TwingTemplateLoadingError, templateLoadingError} from "./error/loader";
import {TwingSandboxSecurityError, sandboxSecurityErrorName} from "./sandbox/security-error";
export type TwingError =
| TwingTemplateLoadingError
| TwingRuntimeError
| TwingParsingError
| TwingSandboxSecurityError
;
export const isATwingError = (candidate: Error): candidate is TwingError => {
return [
templateLoadingError,
parsingErrorName,
runtimeErrorName,
sandboxSecurityErrorName
runtimeErrorName
].includes((candidate as TwingError).name);
};

View File

@ -1,3 +1,5 @@
import type {TwingSource} from "../source";
export type TwingErrorLocation = {
line: number;
column: number;
@ -7,14 +9,14 @@ export interface TwingBaseError<Name extends string> extends Error {
readonly name: Name;
readonly previous: any | undefined;
readonly rootMessage: string;
location: TwingErrorLocation | undefined;
source: string | undefined;
location: TwingErrorLocation;
source: TwingSource;
appendMessage(message: string): void;
}
export const createBaseError = <Name extends string>(
name: Name, message: string, location?: TwingErrorLocation, source?: string, previous?: any
name: Name, message: string, location: TwingErrorLocation, source: TwingSource, previous?: any
): TwingBaseError<Name> => {
const baseError = Error(message);
@ -22,20 +24,10 @@ export const createBaseError = <Name extends string>(
const error = Object.create(baseError, {
location: {
get: () => location,
set: (value: TwingErrorLocation) => {
location = value;
updateRepresentation();
}
get: () => location
},
source: {
get: () => source,
set: (value: string) => {
source = value;
updateRepresentation();
}
get: () => source
},
previous: {
value: previous
@ -69,15 +61,11 @@ export const createBaseError = <Name extends string>(
questionMark = true;
}
if (source) {
representation += ` in "${source}"`;
}
representation += ` in "${source.name}"`;
if (location !== undefined) {
const {line, column} = location;
representation += ` at line ${line}, column ${column}`;
}
const {line, column} = location;
representation += ` at line ${line}, column ${column}`;
if (dot) {
representation += '.';

View File

@ -1,15 +1,4 @@
import {createBaseError, TwingErrorLocation, TwingBaseError} from "./base";
export const templateLoadingError = 'TwingTemplateLoadingError';
/**
* Exception thrown when an error occurs during template loading.
*/
export interface TwingTemplateLoadingError extends TwingBaseError<typeof templateLoadingError> {
}
export const createTemplateLoadingError = (names: Array<string | null>, location?: TwingErrorLocation, source?: string, previous?: any): TwingTemplateLoadingError => {
export const createTemplateLoadingError = (names: Array<string | null>): Error => {
let message: string;
if (names.length === 1) {
@ -20,13 +9,9 @@ export const createTemplateLoadingError = (names: Array<string | null>, location
message = `Unable to find one of the following templates: "${names.join('", "')}".`;
}
const error = createBaseError(templateLoadingError, message, location, source, previous);
const error = Error(message);
Error.captureStackTrace(error, createTemplateLoadingError);
return error;
};
export const isATemplateLoadingError = (candidate: any): candidate is TwingTemplateLoadingError => {
return (candidate as TwingTemplateLoadingError).name === templateLoadingError;
};

View File

@ -1,4 +1,5 @@
import {createBaseError, TwingErrorLocation, TwingBaseError} from "./base";
import type {TwingSource} from "../source";
const Levenshtein = require('levenshtein');
@ -15,7 +16,7 @@ export interface TwingParsingError extends TwingBaseError<typeof parsingErrorNam
}
export const createParsingError = (
message: string, location?: TwingErrorLocation, source?: string, previous?: Error
message: string, location: TwingErrorLocation, source: TwingSource, previous?: Error
): TwingParsingError => {
const baseError = createBaseError(parsingErrorName, message, location, source, previous);

View File

@ -1,4 +1,5 @@
import {createBaseError, TwingErrorLocation, TwingBaseError} from "./base";
import type {TwingSource} from "../source";
export const runtimeErrorName = 'TwingRuntimeError';
@ -6,11 +7,7 @@ export interface TwingRuntimeError extends TwingBaseError<typeof runtimeErrorNam
}
export const isARuntimeError = (candidate: Error): candidate is TwingRuntimeError => {
return (candidate as TwingRuntimeError).name === runtimeErrorName;
};
export const createRuntimeError = (message: string, location?: TwingErrorLocation, source?: string, previous?: Error): TwingRuntimeError => {
export const createRuntimeError = (message: string, location: TwingErrorLocation, source: TwingSource, previous?: Error): TwingRuntimeError => {
const error = createBaseError(runtimeErrorName, message, location, source, previous);
Error.captureStackTrace(error, createRuntimeError);

View File

@ -1,26 +1,12 @@
import type {TwingEscapingStrategyHandler} from "../escaping-strategy";
const phpBin2hex = require("locutus/php/strings/bin2hex");
const phpLtrim = require('locutus/php/strings/ltrim');
const strlen = require('utf8-binary-cutter').getBinarySize;
const phpSprintf = require('locutus/php/strings/sprintf');
export const createCssEscapingStrategyHandler = (): TwingEscapingStrategyHandler => {
return (value) => {
value = value.replace(/[^a-zA-Z0-9]/ug, (matches: string) => {
let char = matches;
// \xHH
if (strlen(char) === 1) {
let hex = phpLtrim(phpBin2hex(char).toUpperCase(), '0');
if (strlen(hex) === 0) {
hex = '0';
}
return '\\' + hex + ' ';
}
// \uHHHH
return '\\' + phpLtrim(phpBin2hex(char).toUpperCase(), '0') + ' ';
value = value.replace(/[^a-zA-Z0-9]/ug, (character: string) => {
const codePoint = character.codePointAt(0)!;
return phpSprintf('\\u%04X', codePoint);
});
return value;

View File

@ -1,8 +1,6 @@
import type {TwingEscapingStrategyHandler} from "../escaping-strategy";
const phpBin2hex = require("locutus/php/strings/bin2hex");
const phpSprintf = require('locutus/php/strings/sprintf');
const strlen = require('utf8-binary-cutter').getBinarySize;
export const createJsEscapingStrategyHandler = (): TwingEscapingStrategyHandler => {
return (value) => {
@ -30,16 +28,22 @@ export const createJsEscapingStrategyHandler = (): TwingEscapingStrategyHandler
return shortMap.get(char);
}
// \uHHHH
char = phpBin2hex(char).toUpperCase();
let codePoint = char.codePointAt(0)!;
if (strlen(char) <= 4) {
return phpSprintf('\\u%04s', char);
if (codePoint <= 0x10000) {
return phpSprintf('\\u%04X', codePoint);
}
// Split characters outside the BMP into surrogate pairs
// https://tools.ietf.org/html/rfc2781.html#section-2.1
codePoint = codePoint - 0x10000;
const high = 0xD800 | (codePoint >> 10);
const low = 0xDC00 | (codePoint & 0x3FF);
return phpSprintf('\\u%04s\\u%04s', char.substr(0, 4), char.substr(4, 4));
return phpSprintf('\\u%04X\\u%04X', high, low);
});
return value;
}
};

View File

@ -1,6 +1,5 @@
import {isTraversable} from "../../../helpers/is-traversable";
import {iteratorToMap} from "../../../helpers/iterator-to-map";
import {createRuntimeError} from "../../../error/runtime";
import {isPlainObject} from "../../../helpers/is-plain-object";
import {TwingCallable} from "../../../callable-wrapper";
@ -16,7 +15,7 @@ export const column: TwingCallable = (_executionContext, thing: any, columnKey:
let map: Map<any, any>;
if (!isTraversable(thing) || isPlainObject(thing)) {
return Promise.reject(createRuntimeError(`The column filter only works with arrays or "Traversable", got "${typeof thing}" as first argument.`));
return Promise.reject(new Error(`The column filter only works with arrays or "Traversable", got "${typeof thing}" as first argument.`));
} else {
map = iteratorToMap(thing);
}

View File

@ -16,6 +16,7 @@ export const escape: TwingCallable<[
const {template, environment} = executionContext;
// todo: probably we need to use traceable method
return escapeValue(template, environment, value, strategy, environment.charset)
.then((value) => {
if (typeof value === "string") {

View File

@ -1,6 +1,5 @@
import {mergeIterables} from "../../../helpers/merge-iterables";
import {isTraversable} from "../../../helpers/is-traversable";
import {createRuntimeError} from "../../../error/runtime";
import {iteratorToMap} from "../../../helpers/iterator-to-map";
import {TwingCallable} from "../../../callable-wrapper";
@ -24,13 +23,13 @@ export const merge: TwingCallable = (_executionContext, iterable1: any, source:
const isIterable1NullOrUndefined = (iterable1 === null) || (iterable1 === undefined);
if (isIterable1NullOrUndefined || (!isTraversable(iterable1) && (typeof iterable1 !== 'object'))) {
return Promise.reject(createRuntimeError(`The merge filter only works on arrays or "Traversable", got "${!isIterable1NullOrUndefined ? typeof iterable1 : iterable1}".`));
return Promise.reject(new Error(`The merge filter only works on arrays or "Traversable", got "${!isIterable1NullOrUndefined ? typeof iterable1 : iterable1}".`));
}
const isSourceNullOrUndefined = (source === null) || (source === undefined);
if (isSourceNullOrUndefined || (!isTraversable(source) && (typeof source !== 'object'))) {
return Promise.reject(createRuntimeError(`The merge filter only accepts arrays or "Traversable" as source, got "${!isSourceNullOrUndefined ? typeof source : source}".`));
return Promise.reject(new Error(`The merge filter only accepts arrays or "Traversable" as source, got "${!isSourceNullOrUndefined ? typeof source : source}".`));
}
return Promise.resolve(mergeIterables(iteratorToMap(iterable1), iteratorToMap(source)));

View File

@ -1,6 +1,5 @@
import {isTraversable} from "../../../helpers/is-traversable";
import {iteratorToHash} from "../../../helpers/iterator-to-hash";
import {createRuntimeError} from "../../../error/runtime";
import {TwingCallable} from "../../../callable-wrapper";
const phpStrtr = require('locutus/php/strings/strtr');
@ -18,7 +17,7 @@ export const replace: TwingCallable = (_executionContext,value: string | null, f
if (isTraversable(from)) {
from = iteratorToHash(from);
} else if (typeof from !== 'object') {
throw createRuntimeError(`The "replace" filter expects an hash or "Iterable" as replace values, got "${typeof from}".`);
throw new Error(`The "replace" filter expects an hash or "Iterable" as replace values, got "${typeof from}".`);
}
if (value === null) {

View File

@ -1,4 +1,3 @@
import {createRuntimeError} from "../../../error/runtime";
import {TwingCallable} from "../../../callable-wrapper";
const phpRound = require('locutus/php/math/round');
@ -21,7 +20,7 @@ export const round: TwingCallable = (_executionContext, value: any, precision: n
}
if (method !== 'ceil' && method !== 'floor') {
throw createRuntimeError('The round filter only supports the "common", "ceil", and "floor" methods.');
throw new Error('The round filter only supports the "common", "ceil", and "floor" methods.');
}
const intermediateValue = value * Math.pow(10, precision);

View File

@ -1,5 +1,4 @@
import {isTraversable} from "../../../helpers/is-traversable";
import {createRuntimeError} from "../../../error/runtime";
import {iteratorToMap} from "../../../helpers/iterator-to-map";
import {asort} from "../../../helpers/asort";
import {TwingCallable} from "../../../callable-wrapper";
@ -18,7 +17,7 @@ export const sort: TwingCallable<[
arrow: ((a: any, b: any) => Promise<-1 | 0 | 1>) | null
], Map<any, any>> = async (_executionContext, iterable, arrow)=> {
if (!isTraversable(iterable)) {
return Promise.reject(createRuntimeError(`The sort filter only works with iterables, got "${typeof iterable}".`));
return Promise.reject(new Error(`The sort filter only works with iterables, got "${typeof iterable}".`));
}
const map = iteratorToMap(iterable);

View File

@ -1,4 +1,3 @@
import {createRuntimeError} from "../../../error/runtime";
import {TwingCallable} from "../../../callable-wrapper";
const phpTrim = require('locutus/php/strings/trim');
@ -26,7 +25,7 @@ export const trim: TwingCallable = (_executionContext, string: string, character
case 'right':
return phpRightTrim(string, characterMask);
default:
throw createRuntimeError('Trimming side must be "left", "right" or "both".');
throw new Error('Trimming side must be "left", "right" or "both".');
}
};

View File

@ -1,6 +1,5 @@
import {DateTime, Duration} from "luxon";
import {modifyDate} from "../../../helpers/modify-date";
import {createRuntimeError} from "../../../error/runtime";
import {TwingCallable} from "../../../callable-wrapper";
/**
@ -72,7 +71,7 @@ export const createDate = (
}
if (!result || !result.isValid) {
throw createRuntimeError(`Failed to parse date "${input}".`);
throw new Error(`Failed to parse date "${input}".`);
}
// now let's apply timezone

View File

@ -1,7 +1,6 @@
import {iteratorToMap} from "../../../helpers/iterator-to-map";
import {mergeIterables} from "../../../helpers/merge-iterables";
import {isTraversable} from "../../../helpers/is-traversable";
import {createRuntimeError} from "../../../error/runtime";
import {isPlainObject} from "../../../helpers/is-plain-object";
import {createContext} from "../../../context";
import {createMarkup, TwingMarkup} from "../../../markup";
@ -44,12 +43,10 @@ export const include: TwingCallable<[
sourceMapRuntime,
strict
} = executionContext;
const from = template.name;
if (!isPlainObject(variables) && !isTraversable(variables)) {
const isVariablesNullOrUndefined = variables === null || variables === undefined;
return Promise.reject(createRuntimeError(`Variables passed to the "include" function or tag must be iterable, got "${!isVariablesNullOrUndefined ? typeof variables : variables}".`, undefined, from));
return Promise.reject(new Error(`Variables passed to the "include" function or tag must be iterable, got "${!isVariablesNullOrUndefined ? typeof variables : variables}".`));
}
variables = iteratorToMap(variables);

View File

@ -1,7 +1,6 @@
import {iconv} from "../../../helpers/iconv";
import {isTraversable} from "../../../helpers/is-traversable";
import {iteratorToArray} from "../../../helpers/iterator-to-array";
import {createRuntimeError} from "../../../error/runtime";
import {TwingCallable} from "../../../callable-wrapper";
const runes = require('runes');
@ -79,7 +78,7 @@ export const random: TwingCallable = (executionContext, values: any | null, max:
}
if (values.length < 1) {
throw createRuntimeError('The random function cannot pick from an empty array.');
return Promise.reject(new Error('The random function cannot pick from an empty array.'));
}
return values[array_rand(values, 1)];

View File

@ -1,5 +1,4 @@
import {isAMarkup, TwingMarkup} from "../markup";
import {createRuntimeError} from "../error/runtime";
import {TwingEnvironment} from "../environment";
import {TwingEscapingStrategy} from "../escaping-strategy";
import {TwingTemplate} from "../template";
@ -28,7 +27,7 @@ export const escapeValue = (
const strategyHandler = environment.escapingStrategyHandlers[strategy];
if (strategyHandler === undefined) {
return Promise.reject(createRuntimeError(`Invalid escaping strategy "${strategy}" (valid ones: ${Object.keys(environment.escapingStrategyHandlers).sort().join(', ')}).`));
return Promise.reject(new Error(`Invalid escaping strategy "${strategy}" (valid ones: ${Object.keys(environment.escapingStrategyHandlers).sort().join(', ')}).`));
}
result = strategyHandler(value.toString(), charset || environment.charset, template.name);

View File

@ -1,5 +1,4 @@
import {isAMapLike} from "./map-like";
import {createRuntimeError} from "../error/runtime";
import {examineObject} from "./examine-object";
import {isPlainObject} from "./is-plain-object";
import {get} from "./get";
@ -124,7 +123,7 @@ export const getAttribute = (
message = `Impossible to access an attribute ("${attribute}") on a ${typeof object} variable ("${object}").`;
}
throw createRuntimeError(message);
throw new Error(message);
}
}
@ -148,7 +147,7 @@ export const getAttribute = (
message = `Impossible to invoke a method ("${attribute}") on a ${typeof object} variable ("${object}").`;
}
throw createRuntimeError(message);
throw new Error(message);
}
// object property
@ -246,7 +245,7 @@ export const getAttribute = (
return;
}
throw createRuntimeError(`Neither the property "${attribute}" nor one of the methods ${attribute}()" or "get${attribute}()"/"is${attribute}()"/"has${attribute}()" exist and have public access in class "${object.constructor.name}".`);
throw new Error(`Neither the property "${attribute}" nor one of the methods ${attribute}()" or "get${attribute}()"/"is${attribute}()"/"has${attribute}()" exist and have public access in class "${object.constructor.name}".`);
}
if (shouldTestExistence) {

View File

@ -1,5 +1,4 @@
import type {TwingContext} from "../context";
import {createRuntimeError} from "../error/runtime";
export const getContextValue = (
charset: string,
@ -40,7 +39,7 @@ export const getContextValue = (
result = context.get(name);
if (result === undefined) {
return Promise.reject(createRuntimeError(`Variable "${name}" does not exist.`));
return Promise.reject(new Error(`Variable "${name}" does not exist.`));
}
}
}

View File

@ -1,20 +1,16 @@
import {isATwingError} from "../error";
import {createRuntimeError} from "../error/runtime";
import type {TwingSource} from "../source";
import {isATwingError} from "../error";
export function getTraceableMethod<M extends (...args: Array<any>) => Promise<any>>(method: M, line: number, column: number, templateName: string): M {
export function getTraceableMethod<M extends (...args: Array<any>) => Promise<any>>(method: M, location: {
line: number;
column: number;
}, templateSource: TwingSource): M {
return ((...args: Array<any>) => {
return method(...args)
.catch((error) => {
if (isATwingError(error)) {
if (error.location === undefined) {
error.location = {line, column};
error.source = templateName;
}
} else {
throw createRuntimeError(`An exception has been thrown during the rendering of a template ("${error.message}").`, {
line,
column
}, templateName, error);
if (!isATwingError(error)) {
throw createRuntimeError(error.message, location, templateSource, error);
}
throw error;

View File

@ -75,7 +75,7 @@ export class TwingLexer extends Lexer {
} catch (error: any) {
const {message, line, column} = (error as SyntaxError);
throw createParsingError(message, {line, column}, source.name, error);
throw createParsingError(message, {line, column}, source, error);
}
}
}

View File

@ -197,7 +197,7 @@ export const executeNode: TwingNodeExecutor = (node, executionContext) => {
executor = executeWithNode;
}
else {
return Promise.reject(createRuntimeError(`Unrecognized node of type "${node.type}"`, node));
return Promise.reject(createRuntimeError(`Unrecognized node of type "${node.type}"`, node, executionContext.template.source));
}
return executor(node, executionContext);

View File

@ -9,7 +9,7 @@ export const executeBlockReferenceNode: TwingNodeExecutor<TwingBlockReferenceNod
} = executionContext;
const {name} = node.attributes;
const displayBlock = getTraceableMethod(template.displayBlock, node.line, node.column, template.name);
const displayBlock = getTraceableMethod(template.displayBlock, node, template.source);
return displayBlock(
{

View File

@ -1,41 +1,34 @@
import {TwingNodeExecutor} from "../node-executor";
import {TwingCheckSecurityNode} from "../node/check-security";
import {
isASandboxSecurityNotAllowedFilterError,
TwingSandboxSecurityNotAllowedFilterError
} from "../sandbox/security-not-allowed-filter-error";
import {TwingSandboxSecurityNotAllowedFunctionError} from "../sandbox/security-not-allowed-function-error";
import {
isASandboxSecurityNotAllowedTagError,
TwingSandboxSecurityNotAllowedTagError
} from "../sandbox/security-not-allowed-tag-error";
import type {TwingNode} from "../node";
import {createRuntimeError} from "../error/runtime";
export const executeCheckSecurityNode: TwingNodeExecutor<TwingCheckSecurityNode> = (node, executionContext) => {
const {template, environment, sandboxed} = executionContext;
const {usedTags, usedFunctions, usedFilters} = node.attributes;
try {
sandboxed && environment.sandboxPolicy.checkSecurity(
if (sandboxed) {
const issue = environment.sandboxPolicy.checkSecurity(
[...usedTags.keys()],
[...usedFilters.keys()],
[...usedFunctions.keys()]
);
} catch (error: any) {
const supplementError = (error: TwingSandboxSecurityNotAllowedFilterError | TwingSandboxSecurityNotAllowedFunctionError | TwingSandboxSecurityNotAllowedTagError) => {
error.source = template.name;
if (isASandboxSecurityNotAllowedTagError(error)) {
error.location = usedTags.get(error.tagName);
} else if (isASandboxSecurityNotAllowedFilterError(error)) {
error.location = usedFilters.get(error.filterName)
if (issue !== null) {
const {type, token} = issue;
let node: TwingNode;
if (type === "tag") {
node = usedTags.get(token)!;
} else if (type === "filter") {
node = usedFilters.get(token)!
} else {
error.location = usedFunctions.get(error.functionName);
node = usedFunctions.get(token)!;
}
throw createRuntimeError(issue.message, node, template.source);
}
supplementError(error);
throw error;
}
return Promise.resolve();

View File

@ -20,7 +20,7 @@ export const executeCheckToStringNode: TwingNodeExecutor<TwingCheckToStringNode>
}
return Promise.resolve(value);
}, valueNode.line, valueNode.column, template.name)
}, valueNode, template.source)
return assertToStringAllowed(value);
}

View File

@ -13,7 +13,7 @@ export const executeAttributeAccessorNode: TwingNodeExecutor<TwingAttributeAcces
execute(attribute, executionContext),
execute(methodArguments, executionContext)
]).then(([target, attribute, methodArguments]) => {
const traceableGetAttribute = getTraceableMethod(getAttribute, node.line, node.column, template.name);
const traceableGetAttribute = getTraceableMethod(getAttribute, node, template.source);
return traceableGetAttribute(
environment,

View File

@ -10,7 +10,7 @@ import {createRuntimeError} from "../../error/runtime";
export const executeBinaryNode: TwingNodeExecutor<TwingBaseBinaryNode<any>> = async (node, executionContext) => {
const {left, right} = node.children;
const {nodeExecutor: execute} = executionContext;
const {nodeExecutor: execute, template} = executionContext;
switch (node.type) {
case "add": {
@ -161,5 +161,5 @@ export const executeBinaryNode: TwingNodeExecutor<TwingBaseBinaryNode<any>> = as
}
}
return Promise.reject(createRuntimeError(`Unrecognized binary node of type "${node.type}"`, node));
return Promise.reject(createRuntimeError(`Unrecognized binary node of type "${node.type}"`, node, template.source));
};

View File

@ -22,9 +22,8 @@ export const executeBlockFunction: TwingNodeExecutor<TwingBlockFunctionNode> = a
const loadTemplate = getTraceableMethod(
template.loadTemplate,
templateNode.line,
templateNode.column,
template.name
templateNode,
template.source
);
resolveTemplate = loadTemplate(executionContext, templateName);
@ -35,14 +34,14 @@ export const executeBlockFunction: TwingNodeExecutor<TwingBlockFunctionNode> = a
return resolveTemplate
.then<Promise<boolean | string>>((templateOfTheBlock) => {
if (node.attributes.shouldTestExistence) {
const hasBlock = getTraceableMethod(templateOfTheBlock.hasBlock, node.line, node.column, template.name);
const hasBlock = getTraceableMethod(templateOfTheBlock.hasBlock, node, template.source);
return hasBlock({
...executionContext,
context: context.clone()
}, blockName, blocks);
} else {
const displayBlock = getTraceableMethod(templateOfTheBlock.displayBlock, node.line, node.column, template.name);
const displayBlock = getTraceableMethod(templateOfTheBlock.displayBlock, node, template.source);
let useBlocks = templateNode === undefined;

View File

@ -11,6 +11,7 @@ import {getKeyValuePairs} from "../../helpers/get-key-value-pairs";
import {getTest} from "../../helpers/get-test";
import {getFunction} from "../../helpers/get-function";
import {getFilter} from "../../helpers/get-filter";
import type {TwingTemplate} from "../../template";
const array_merge = require('locutus/php/array/array_merge');
const snakeCase = require('snake-case');
@ -21,6 +22,7 @@ const normalizeName = (name: string) => {
const getArguments = (
node: TwingBaseCallNode<any>,
template: TwingTemplate,
argumentsNode: TwingArrayNode,
acceptedArguments: Array<TwingCallableArgument>,
isVariadic: boolean
@ -44,7 +46,7 @@ const getArguments = (
name = normalizeName(name);
}
else if (named) {
throw createRuntimeError(`Positional arguments cannot be used after named arguments for ${callType} "${callName}".`, node);
throw createRuntimeError(`Positional arguments cannot be used after named arguments for ${callType} "${callName}".`, node, template.source);
}
parameters.set(name, {
@ -69,7 +71,7 @@ const getArguments = (
if (parameter) {
if (parameters.has(position)) {
throw createRuntimeError(`Argument "${name}" is defined twice for ${callType} "${callName}".`, node);
throw createRuntimeError(`Argument "${name}" is defined twice for ${callType} "${callName}".`, node, template.source);
}
arguments_ = array_merge(arguments_, optionalArguments);
@ -91,7 +93,7 @@ const getArguments = (
arguments_.push(createConstantNode(callableParameter.defaultValue, node.line, node.column));
}
else {
throw createRuntimeError(`Value for argument "${name}" is required for ${callType} "${callName}".`, node);
throw createRuntimeError(`Value for argument "${name}" is required for ${callType} "${callName}".`, node, template.source);
}
}
}
@ -119,7 +121,7 @@ const getArguments = (
if (parameters.size > 0) {
const unknownParameter = [...parameters.values()][0];
throw createRuntimeError(`Unknown argument${parameters.size > 1 ? 's' : ''} "${[...parameters.keys()].join('", "')}" for ${callType} "${callName}(${names.join(', ')})".`, unknownParameter.key);
throw createRuntimeError(`Unknown argument${parameters.size > 1 ? 's' : ''} "${[...parameters.keys()].join('", "')}" for ${callType} "${callName}(${names.join(', ')})".`, unknownParameter.key, template.source);
}
return arguments_;
@ -149,13 +151,14 @@ export const executeCallNode: TwingNodeExecutor<TwingBaseCallNode<any>> = async
}
if (callableWrapper === null) {
throw createRuntimeError(`Unknown ${type} "${operatorName}".`, node);
throw createRuntimeError(`Unknown ${type} "${operatorName}".`, node, template.source);
}
const {operand, arguments: callArguments} = node.children;
const argumentNodes = getArguments(
node,
template,
callArguments,
callableWrapper.acceptedArguments,
callableWrapper.isVariadic
@ -175,7 +178,7 @@ export const executeCallNode: TwingNodeExecutor<TwingBaseCallNode<any>> = async
actualArguments.push(...providedArguments);
const traceableCallable = getTraceableMethod(callableWrapper.callable, node.line, node.column, template.name);
const traceableCallable = getTraceableMethod(callableWrapper.callable, node, template.source);
return traceableCallable(executionContext, ...actualArguments).then((value) => {
return value;

View File

@ -10,7 +10,7 @@ export const executeEscapeNode: TwingNodeExecutor<TwingEscapeNode> = (node, exec
return execute(body, executionContext)
.then((value) => {
const traceableEscape = getTraceableMethod(escapeValue, node.line, node.column, template.name);
const traceableEscape = getTraceableMethod(escapeValue, node, template.source);
return traceableEscape(template, environment, value, strategy, null);
});

View File

@ -47,7 +47,7 @@ export const executeMethodCall: TwingNodeExecutor<TwingMethodCallNode> = async (
if (handler) {
return handler(executionContext, ...macroArguments);
} else {
throw createRuntimeError(`Macro "${methodName}" is not defined in template "${macroTemplate.name}".`, node, template.name);
throw createRuntimeError(`Macro "${methodName}" is not defined in template "${macroTemplate.name}".`, node, template.source);
}
});
}

View File

@ -15,9 +15,8 @@ export const executeNameNode: TwingNodeExecutor<TwingNameNode> = (node, {
const traceableGetContextValue = getTraceableMethod(
getContextValue,
node.line,
node.column,
template.name
node,
template.source
);
return traceableGetContextValue(

View File

@ -5,7 +5,7 @@ import {getTraceableMethod} from "../../helpers/traceable-method";
export const executeParentFunction: TwingNodeExecutor<TwingParentFunctionNode> = (node, executionContext) => {
const {template, outputBuffer} = executionContext;
const {name} = node.attributes;
const displayParentBlock = getTraceableMethod(template.displayParentBlock, node.line, node.column, template.name);
const displayParentBlock = getTraceableMethod(template.displayParentBlock, node, template.source);
outputBuffer.start();

View File

@ -4,7 +4,7 @@ import {createRuntimeError} from "../../error/runtime";
export const executeUnaryNode: TwingNodeExecutor<TwingBaseUnaryNode<any>> = (node, executionContext) => {
const {operand} = node.children;
const {nodeExecutor: execute} = executionContext;
const {nodeExecutor: execute, template} = executionContext;
switch (node.type) {
case "negative": {
@ -18,5 +18,5 @@ export const executeUnaryNode: TwingNodeExecutor<TwingBaseUnaryNode<any>> = (nod
}
}
return Promise.reject(createRuntimeError(`Unrecognized unary node of type "${node.type}"`, node));
return Promise.reject(createRuntimeError(`Unrecognized unary node of type "${node.type}"`, node, template.source));
};

View File

@ -17,7 +17,7 @@ export const executeImportNode: TwingNodeExecutor<TwingImportNode> = async (node
} else {
const templateName = await execute(templateNameNode, executionContext);
const loadTemplate = getTraceableMethod(template.loadTemplate, node.line, node.column, template.name);
const loadTemplate = getTraceableMethod(template.loadTemplate, node, template.source);
aliasValue = await loadTemplate(executionContext, templateName);
}

View File

@ -15,7 +15,7 @@ export const executeBaseIncludeNode = async (
const templatesToInclude = await getTemplate(executionContext);
const traceableInclude = getTraceableMethod(include, node.line, node.column, template.name);
const traceableInclude = getTraceableMethod(include, node, template.source);
const output = await traceableInclude(
executionContext,

View File

@ -14,7 +14,7 @@ export const executeEmbedNode: TwingNodeExecutor<TwingEmbedNode> = (node, execut
const embeddedTemplate = embeddedTemplates.get(index)!;
return Promise.resolve(embeddedTemplate);
}, node.line, node.column, template.name);
}, node, template.source);
return loadEmbeddedTemplate();
})

View File

@ -16,7 +16,7 @@ export const executeWithNode: TwingNodeExecutor<TwingWithNode> = async (node, ex
const variables = await execute(variablesNode, executionContext);
if (typeof variables !== "object") {
throw createRuntimeError(`Variables passed to the "with" tag must be a hash.`, node, template.name);
throw createRuntimeError(`Variables passed to the "with" tag must be a hash.`, node, template.source);
}
if (only) {

View File

@ -1,21 +1,22 @@
import {TwingNodeVisitor} from "./node-visitor";
import {TwingBaseNode, getChildren} from "./node";
import type {TwingSource} from "./source";
/**
* TwingNodeTraverser is a node traverser.
*
* It visits all nodes and their children and calls the registered visitors for each.
*/
export type TwingNodeTraverser = (node: TwingBaseNode) => TwingBaseNode | null;
export type TwingNodeTraverser = (node: TwingBaseNode, source: TwingSource) => TwingBaseNode | null;
export const createNodeTraverser = (
visitors: Array<TwingNodeVisitor>
): TwingNodeTraverser => {
const traverseWithVisitor = (visitor: TwingNodeVisitor, node: TwingBaseNode) => {
node = visitor.enterNode(node);
const traverseWithVisitor = (visitor: TwingNodeVisitor, node: TwingBaseNode, source: TwingSource) => {
node = visitor.enterNode(node, source);
for (const [key, child] of getChildren(node)) {
const newChild = traverseWithVisitor(visitor, child);
const newChild = traverseWithVisitor(visitor, child, source);
if (newChild) {
if (newChild !== child) {
@ -26,14 +27,14 @@ export const createNodeTraverser = (
}
}
return visitor.leaveNode(node);
return visitor.leaveNode(node, source);
};
return (node) => {
return (node, template) => {
let result: TwingBaseNode | null = node;
for (const visitor of visitors) {
result = traverseWithVisitor(visitor, node);
result = traverseWithVisitor(visitor, node, template);
}
return result;

View File

@ -1,4 +1,5 @@
import type {TwingBaseNode} from "./node";
import type {TwingSource} from "./source";
/**
* The interface that all node visitors must implement.
@ -9,22 +10,22 @@ export interface TwingNodeVisitor {
*
* @return The modified node
*/
enterNode(node: TwingBaseNode): TwingBaseNode;
enterNode(node: TwingBaseNode, source: TwingSource): TwingBaseNode;
/**
* Called after the passed node has been visited.
*
* @return The modified node or null if the node must be removed from its parent
*/
leaveNode(node: TwingBaseNode): TwingBaseNode | null;
leaveNode(node: TwingBaseNode, source: TwingSource): TwingBaseNode | null;
}
/**
* Convenient factory for TwingNodeVisitor
*/
export const createNodeVisitor = (
enterNode: (node: TwingBaseNode) => TwingBaseNode,
leaveNode: (node: TwingBaseNode) => TwingBaseNode | null
enterNode: (node: TwingBaseNode, source: TwingSource) => TwingBaseNode,
leaveNode: (node: TwingBaseNode, source: TwingSource) => TwingBaseNode | null
): TwingNodeVisitor => {
return {
enterNode,

View File

@ -15,6 +15,7 @@ import {createConditionalNode} from "../node/expression/conditional";
import {TwingFilterNode} from "../node/expression/call/filter";
import type {TwingNode} from "../node";
import {getKeyValuePairs} from "../helpers/get-key-value-pairs";
import type {TwingSource} from "../source";
export const createCoreNodeVisitor = (): TwingNodeVisitor => {
const enteredNodes: Array<TwingBaseExpressionNode> = [];
@ -48,7 +49,7 @@ export const createCoreNodeVisitor = (): TwingNodeVisitor => {
return newNode;
};
const enterDefinedTestNode = (node: TwingTestNode): TwingTestNode => {
const enterDefinedTestNode = (node: TwingTestNode, source: TwingSource): TwingTestNode => {
const operand = node.children.operand! as TwingNode;
if (
@ -61,7 +62,7 @@ export const createCoreNodeVisitor = (): TwingNodeVisitor => {
operand.type !== "method_call" &&
!(operand.type === "function" && operand.attributes.operatorName === 'constant')
) {
throw createParsingError('The "defined" test only works with simple variables.', node);
throw createParsingError('The "defined" test only works with simple variables.', node, source);
}
let newOperand: TwingBaseExpressionNode;
@ -139,7 +140,7 @@ export const createCoreNodeVisitor = (): TwingNodeVisitor => {
};
return {
enterNode: (node: TwingNode) => {
enterNode: (node: TwingNode, source) => {
if (!enteredNodes.includes(node)) {
enteredNodes.push(node);
@ -151,7 +152,7 @@ export const createCoreNodeVisitor = (): TwingNodeVisitor => {
if (node.type === "test") {
if (node.attributes.operatorName === "defined") {
return enterDefinedTestNode(node);
return enterDefinedTestNode(node, source);
}
}
}

View File

@ -1,7 +1,7 @@
import type {TwingTokenStream} from "./token-stream";
import {TwingTagHandler, TwingTokenParser} from "./tag-handler";
import {TwingNodeVisitor} from "./node-visitor";
import {createParsingError, TwingParsingError} from "./error/parsing";
import {createParsingError} from "./error/parsing";
import {TwingBaseNode, getChildren, TwingNode, createNode} from "./node";
import {createTextNode} from "./node/text";
import {createPrintNode} from "./node/print";
@ -309,7 +309,7 @@ export const createParser = (
throw createParsingError(
`A template that extends another one cannot include content outside Twig blocks. Did you forget to put the content inside a {% block %} tag?`,
node,
stream.source.name
stream.source
);
}
@ -330,7 +330,7 @@ export const createParser = (
// expected as the definition is not part of the default template code flow.
if (nested && (type === "block_reference")) {
if (level >= 3) {
throw createParsingError(`A block definition cannot be nested under non-capturing nodes.`, node, stream.source.name);
throw createParsingError(`A block definition cannot be nested under non-capturing nodes.`, node, stream.source);
}
else {
console.warn(`Nesting a block definition under a non-capturing node in "${stream.source.name}" at line ${node.line} is deprecated since Twig 2.5.0 and will become a syntax error in Twig 3.0.`);
@ -388,7 +388,7 @@ export const createParser = (
}
}
else if (strict) {
const error = createParsingError(`Unknown filter "${name}".`, {line, column}, stream.source.name);
const error = createParsingError(`Unknown filter "${name}".`, {line, column}, stream.source);
error.addSuggestions(name, filterNames);
@ -421,7 +421,7 @@ export const createParser = (
}
}
else if (strict) {
const error = createParsingError(`Unknown function "${name}".`, {line, column}, stream.source.name);
const error = createParsingError(`Unknown function "${name}".`, {line, column}, stream.source);
error.addSuggestions(name, functionNames);
@ -440,14 +440,14 @@ export const createParser = (
throw createParsingError('Calling "parent" outside a block is forbidden.', {
line,
column
}, stream.source.name);
}, stream.source);
}
if (!parent && !hasTraits()) {
throw createParsingError('Calling "parent" on a template that does not extend nor "use" another template is forbidden.', {
line,
column
}, stream.source.name);
}, stream.source);
}
return createParentFunctionNode(peekBlockStack(), line, column);
@ -459,7 +459,7 @@ export const createParser = (
throw createParsingError('The "block" function takes one argument (the block name).', {
line,
column
}, stream.source.name);
}, stream.source);
}
return createBlockFunctionNode(keyValuePairs[0].value, keyValuePairs.length > 1 ? keyValuePairs[1].value : null, line, column);
@ -471,7 +471,7 @@ export const createParser = (
throw createParsingError('The "attribute" function takes at least two arguments (the variable and the attributes).', {
line,
column
}, stream.source.name);
}, stream.source);
}
return createAttributeAccessorNode(
@ -639,7 +639,7 @@ export const createParser = (
return name;
}
const error = createParsingError(`Unknown test "${name}".`, {line, column}, stream.source.name);
const error = createParsingError(`Unknown test "${name}".`, {line, column}, stream.source);
error.addSuggestions(name, testNames);
@ -702,20 +702,10 @@ export const createParser = (
}];
embeddedTemplates = [];
let body: TwingBaseNode | null;
let body: TwingBaseNode | null = subparse(stream, tag, test);
try {
body = subparse(stream, tag, test);
if (parent !== null && (body = filterChildBodyNode(stream, body)) === null) {
body = createNode();
}
} catch (error: any) {
if (!(error as TwingParsingError).source) {
(error as TwingParsingError).source = stream.source.name;
}
throw error;
if (parent !== null && (body = filterChildBodyNode(stream, body)) === null) {
body = createNode();
}
let node = createTemplateNode(
@ -732,7 +722,7 @@ export const createParser = (
// passed visitors
let traverse = createNodeTraverser(visitors);
node = traverse(node) as TwingTemplateNode;
node = traverse(node, stream.source) as TwingTemplateNode;
// core visitors
traverse = createNodeTraverser([
@ -741,7 +731,7 @@ export const createParser = (
createSandboxNodeVisitor()
]);
node = traverse(node) as TwingTemplateNode;
node = traverse(node, stream.source) as TwingTemplateNode;
// restore previous stack so previous parse() call can resume working
const previousStackEntry = stack.pop()!;
@ -804,7 +794,7 @@ export const createParser = (
if (namedArguments && (token = stream.nextIf("OPERATOR", '='))) {
if (value.type !== "name") {
throw createParsingError(`A parameter name must be a string, "${value.type.toString()}" given.`, value, stream.source.name);
throw createParsingError(`A parameter name must be a string, "${value.type.toString()}" given.`, value, stream.source);
}
key = createConstantNode(value.attributes.name, value.line, value.column);
@ -815,7 +805,7 @@ export const createParser = (
const notConstantNode = checkConstantExpression(stream, value);
if (notConstantNode !== null) {
throw createParsingError(`A default value for an argument must be a constant (a boolean, a string, a number, or an array).`, notConstantNode, stream.source.name);
throw createParsingError(`A default value for an argument must be a constant (a boolean, a string, a number, or an array).`, notConstantNode, stream.source);
}
}
else {
@ -906,7 +896,7 @@ export const createParser = (
let value = token.value;
if (['true', 'false', 'none', 'null'].indexOf(value.toLowerCase()) > -1) {
throw createParsingError(`You cannot assign a value to "${value}".`, token, stream.source.name);
throw createParsingError(`You cannot assign a value to "${value}".`, token, stream.source);
}
pushToRecord(targets, createAssignmentNode(value, token.line, token.column));
@ -979,7 +969,7 @@ export const createParser = (
token = stream.current;
if (!token.test("NAME")) {
throw createParsingError(`Unexpected token "${typeToEnglish(token.type)}" of value "${token.value}".`, token, stream.source.name);
throw createParsingError(`Unexpected token "${typeToEnglish(token.type)}" of value "${token.value}".`, token, stream.source);
}
names[i++] = createAssignmentNode(token.value, token.line, token.column);
@ -1216,7 +1206,7 @@ export const createParser = (
throw createParsingError(`A hash key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "${typeToEnglish(type)}" of value "${value}".`, {
line,
column
}, stream.source.name);
}, stream.source);
}
stream.expect("PUNCTUATION", ':', 'A hash key must be followed by a colon (:)');
@ -1351,10 +1341,10 @@ export const createParser = (
node = parseHashExpression(stream);
}
else if (token.test("OPERATOR", '=') && (stream.look(-1).value === '==' || stream.look(-1).value === '!=')) {
throw createParsingError(`Unexpected operator of value "${token.value}". Did you try to use "===" or "!==" for strict comparison? Use "is same as(value)" instead.`, token, stream.source.name);
throw createParsingError(`Unexpected operator of value "${token.value}". Did you try to use "===" or "!==" for strict comparison? Use "is same as(value)" instead.`, token, stream.source);
}
else {
throw createParsingError(`Unexpected token "${typeToEnglish(token.type)}" of value "${token.value}".`, token, stream.source.name);
throw createParsingError(`Unexpected token "${typeToEnglish(token.type)}" of value "${token.value}".`, token, stream.source);
}
}
@ -1428,7 +1418,7 @@ export const createParser = (
}
}
else {
throw createParsingError('Expected name or number.', {line, column: column + 1}, stream.source.name);
throw createParsingError('Expected name or number.', {line, column: column + 1}, stream.source);
}
if ((node.type === "name") && (node.attributes.name === '_self' || getImportedTemplate(node.attributes.name))) {
@ -1582,7 +1572,7 @@ export const createParser = (
token = stream.current;
if (token.type !== "NAME") {
throw createParsingError('A block must start with a tag name.', token, stream.source.name);
throw createParsingError('A block must start with a tag name.', token, stream.source);
}
if ((test !== null) && test(token)) {
@ -1600,7 +1590,7 @@ export const createParser = (
error = createParsingError(
`Unexpected "${token.value}" tag`,
token,
stream.source.name
stream.source
);
error.appendMessage(` (expecting closing tag for the "${tag}" tag defined line ${line}).`);
@ -1609,7 +1599,7 @@ export const createParser = (
error = createParsingError(
`Unknown "${token.value}" tag.`,
token,
stream.source.name
stream.source
);
error.addSuggestions(token.value, tags);

View File

@ -1,30 +0,0 @@
import type {TwingSandboxSecurityNotAllowedFilterError} from "./security-not-allowed-filter-error";
import type {TwingSandboxSecurityNotAllowedFunctionError} from "./security-not-allowed-function-error";
import type {TwingSandboxSecurityNotAllowedMethodError} from "./security-not-allowed-method-error";
import type {TwingSandboxSecurityNotAllowedPropertyError} from "./security-not-allowed-property-error";
import type {TwingSandboxSecurityNotAllowedTagError} from "./security-not-allowed-tag-error";
import {createBaseError, TwingErrorLocation, TwingBaseError} from "../error/base";
export type TwingSandboxSecurityError =
| TwingSandboxSecurityNotAllowedFilterError
| TwingSandboxSecurityNotAllowedFunctionError
| TwingSandboxSecurityNotAllowedMethodError
| TwingSandboxSecurityNotAllowedPropertyError
| TwingSandboxSecurityNotAllowedTagError
;
export const sandboxSecurityErrorName = 'TwingSandboxSecurityError';
/**
* Exception thrown when a security error occurs at runtime.
*/
export interface BaseSandboxSecurityError extends TwingBaseError<typeof sandboxSecurityErrorName> {
}
export const createBaseSandboxSecurityError = (message: string, location?: TwingErrorLocation, source?: string) => {
const error = createBaseError(sandboxSecurityErrorName, message, location, source);
Error.captureStackTrace(error, createBaseSandboxSecurityError);
return error;
};

View File

@ -1,29 +0,0 @@
import {
BaseSandboxSecurityError,
createBaseSandboxSecurityError,
TwingSandboxSecurityError
} from "./security-error";
import {TwingErrorLocation} from "../error/base";
/**
* Exception thrown when a not allowed filter is used in a template.
*/
export interface TwingSandboxSecurityNotAllowedFilterError extends BaseSandboxSecurityError {
readonly filterName: string;
}
export const createSandboxSecurityNotAllowedFilterError = (message: string, filterName: string, location?: TwingErrorLocation, source?: string): TwingSandboxSecurityNotAllowedFilterError => {
const error = createBaseSandboxSecurityError(message, location, source);
Error.captureStackTrace(error, createSandboxSecurityNotAllowedFilterError);
return Object.assign(error, {
get filterName() {
return filterName;
}
});
};
export const isASandboxSecurityNotAllowedFilterError = (candidate: TwingSandboxSecurityError): candidate is TwingSandboxSecurityNotAllowedFilterError => {
return (candidate as TwingSandboxSecurityNotAllowedFilterError).filterName !== undefined;
};

View File

@ -1,18 +0,0 @@
import {BaseSandboxSecurityError, createBaseSandboxSecurityError} from "./security-error";
import {TwingErrorLocation} from "../error/base";
export interface TwingSandboxSecurityNotAllowedFunctionError extends BaseSandboxSecurityError {
readonly functionName: string;
}
export const createSandboxSecurityNotAllowedFunctionError = (message: string, functionName: string, location?: TwingErrorLocation, source?: string): TwingSandboxSecurityNotAllowedFunctionError => {
const error = createBaseSandboxSecurityError(message, location, source);
Error.captureStackTrace(error, createSandboxSecurityNotAllowedFunctionError);
return Object.assign(error, {
get functionName() {
return functionName;
}
});
};

View File

@ -1,13 +0,0 @@
import {BaseSandboxSecurityError, createBaseSandboxSecurityError} from "./security-error";
import {TwingErrorLocation} from "../error/base";
export interface TwingSandboxSecurityNotAllowedMethodError extends BaseSandboxSecurityError {
}
export const createSandboxSecurityNotAllowedMethodError = (message: string, location?: TwingErrorLocation, source?: string): TwingSandboxSecurityNotAllowedMethodError => {
const error = createBaseSandboxSecurityError(message, location, source);
Error.captureStackTrace(error, createSandboxSecurityNotAllowedMethodError);
return error;
};

View File

@ -1,16 +0,0 @@
import {BaseSandboxSecurityError, createBaseSandboxSecurityError} from "./security-error";
import {TwingErrorLocation} from "../error/base";
/**
* Exception thrown when a not allowed object property is used in a template.
*/
export interface TwingSandboxSecurityNotAllowedPropertyError extends BaseSandboxSecurityError {
}
export const createSandboxSecurityNotAllowedPropertyError = (message: string, location?: TwingErrorLocation, source?: string): TwingSandboxSecurityNotAllowedPropertyError => {
const error = createBaseSandboxSecurityError(message, location, source);
Error.captureStackTrace(error, createSandboxSecurityNotAllowedPropertyError);
return error;
};

View File

@ -1,23 +0,0 @@
import {BaseSandboxSecurityError, createBaseSandboxSecurityError, TwingSandboxSecurityError} from "./security-error";
import {TwingSandboxSecurityNotAllowedPropertyError} from "./security-not-allowed-property-error";
import {TwingErrorLocation} from "../error/base";
export interface TwingSandboxSecurityNotAllowedTagError extends BaseSandboxSecurityError {
readonly tagName: string;
}
export const createSandboxSecurityNotAllowedTagError = (message: string, tagName: string, location?: TwingErrorLocation, source?: string): TwingSandboxSecurityNotAllowedPropertyError => {
const error = createBaseSandboxSecurityError(message, location, source);
Error.captureStackTrace(error, createSandboxSecurityNotAllowedTagError);
return Object.assign(error, {
get tagName() {
return tagName;
}
});
};
export const isASandboxSecurityNotAllowedTagError = (candidate: TwingSandboxSecurityError): candidate is TwingSandboxSecurityNotAllowedTagError => {
return (candidate as TwingSandboxSecurityNotAllowedTagError).tagName !== undefined;
};

View File

@ -1,8 +1,3 @@
import {createSandboxSecurityNotAllowedFilterError} from "./security-not-allowed-filter-error";
import {createSandboxSecurityNotAllowedTagError} from "./security-not-allowed-tag-error";
import {createSandboxSecurityNotAllowedFunctionError} from "./security-not-allowed-function-error";
import {createSandboxSecurityNotAllowedPropertyError} from "./security-not-allowed-property-error";
import {createSandboxSecurityNotAllowedMethodError} from "./security-not-allowed-method-error";
import {isAMarkup, TwingMarkup} from "../markup";
export interface TwingSandboxSecurityPolicy {
@ -22,7 +17,11 @@ export interface TwingSandboxSecurityPolicy {
*/
checkPropertyAllowed(candidate: any | TwingMarkup, property: string): void;
checkSecurity(tags: Array<string>, filters: Array<string>, functions: Array<string>): void;
checkSecurity(tags: Array<string>, filters: Array<string>, functions: Array<string>): {
message: string;
token: string;
type: "filter" | "function" | "tag"
} | null;
}
export const createSandboxSecurityPolicy = (
@ -51,15 +50,15 @@ export const createSandboxSecurityPolicy = (
for (const [constructorName, methods] of allowedMethods) {
if (candidate instanceof constructorName) {
allowed = methods.includes(method);
break;
}
}
if (!allowed) {
const constructorName = candidate.constructor.name || '(anonymous)';
throw createSandboxSecurityNotAllowedMethodError(`Calling "${method}" method on an instance of ${constructorName} is not allowed.`);
throw new Error(`Calling "${method}" method on an instance of ${constructorName} is not allowed.`);
}
},
checkPropertyAllowed: (candidate, property) => {
@ -76,27 +75,41 @@ export const createSandboxSecurityPolicy = (
if (!allowed) {
const constructorName = candidate.constructor.name || '(anonymous)';
throw createSandboxSecurityNotAllowedPropertyError(`Calling "${property}" property on an instance of ${constructorName} is not allowed.`);
throw new Error(`Calling "${property}" property on an instance of ${constructorName} is not allowed.`);
}
},
checkSecurity: (tags, filters, functions) => {
for (const tag of tags) {
if (!allowedTags.includes(tag)) {
throw createSandboxSecurityNotAllowedTagError(`Tag "${tag}" is not allowed.`, tag);
for (const tagName of tags) {
if (!allowedTags.includes(tagName)) {
return ({
message: `Tag "${tagName}" is not allowed.`,
token: tagName,
type: "tag"
});
}
}
for (const filterName of filters) {
if (!allowedFilters.includes(filterName)) {
throw createSandboxSecurityNotAllowedFilterError(`Filter "${filterName}" is not allowed.`, filterName);
return ({
message: `Filter "${filterName}" is not allowed.`,
token: filterName,
type: "filter"
});
}
}
for (const function_ of functions) {
if (!allowedFunctions.includes(function_)) {
throw createSandboxSecurityNotAllowedFunctionError(`Function "${function_}" is not allowed.`, function_);
for (const functionName of functions) {
if (!allowedFunctions.includes(functionName)) {
return ({
message: `Function "${functionName}" is not allowed.`,
token: functionName,
type: "function"
});
}
}
return null;
}
}
;

View File

@ -28,7 +28,7 @@ export const createAutoEscapeTagHandler = (): TwingTagHandler => {
) {
const {line, column} = expression;
throw createParsingError('An escaping strategy must be a string or false.', {line, column}, stream.source.name);
throw createParsingError('An escaping strategy must be a string or false.', {line, column}, stream.source);
}
const {value} = expression.attributes;

View File

@ -29,7 +29,7 @@ export const createBlockTagHandler = (): TwingTagHandler => {
let block = parser.getBlock(name);
if (block !== null) {
throw createParsingError(`The block '${name}' has already been defined at {${block.line}:${block.column}}.`, {line, column}, stream.source.name);
throw createParsingError(`The block '${name}' has already been defined at {${block.line}:${block.column}}.`, {line, column}, stream.source);
}
block = createBlockNode(name, createNode(), line, column);
@ -55,7 +55,7 @@ export const createBlockTagHandler = (): TwingTagHandler => {
if (value !== name) {
const {line, column} = token;
throw createParsingError(`Expected endblock for block "${name}" (but "${value}" given).`, {line, column}, stream.source.name);
throw createParsingError(`Expected endblock for block "${name}" (but "${value}" given).`, {line, column}, stream.source);
}
}
} else {

View File

@ -22,13 +22,13 @@ export const createExtendsTagHandler = (): TwingTagHandler => {
const {line, column} = token;
if (parser.peekBlockStack()) {
throw createParsingError('Cannot use "extend" in a block.', {line, column}, stream.source.name);
throw createParsingError('Cannot use "extend" in a block.', {line, column}, stream.source);
} else if (!parser.isMainScope()) {
throw createParsingError('Cannot use "extend" in a macro.', {line, column}, stream.source.name);
throw createParsingError('Cannot use "extend" in a macro.', {line, column}, stream.source);
}
if (parser.parent !== null) {
throw createParsingError('Multiple extends tags are forbidden.', {line, column}, stream.source.name);
throw createParsingError('Multiple extends tags are forbidden.', {line, column}, stream.source);
}
parser.parent = parser.parseExpression(stream);

View File

@ -31,7 +31,7 @@ export const createForTagHandler = (): TwingTagHandler => {
// the loop variable cannot be used in the condition
const checkLoopUsageCondition = (stream: TwingTokenStream, node: TwingNode) => {
if ((node.type === "attribute_accessor") && (node.children.target.type === "name") && (node.children.target.attributes.name === 'loop')) {
throw createParsingError('The "loop" variable cannot be used in a looping condition.', node, stream.source.name);
throw createParsingError('The "loop" variable cannot be used in a looping condition.', node, stream.source);
}
for (const [, child] of getChildren(node)) {
@ -46,7 +46,7 @@ export const createForTagHandler = (): TwingTagHandler => {
const {attribute} = node.children;
if (attribute.type === "constant" && (['length', 'revindex0', 'revindex', 'last'].indexOf(attribute.attributes.value as string) > -1)) {
throw createParsingError(`The "loop.${attribute.attributes.value}" variable is not defined when looping with a condition.`, node, stream.source.name);
throw createParsingError(`The "loop.${attribute.attributes.value}" variable is not defined when looping with a condition.`, node, stream.source);
}
}

View File

@ -29,7 +29,7 @@ export const createMacroTagHandler = (): TwingTagHandler => {
const {value: argumentName} = key.attributes;
if (argumentName === VARARGS_NAME) {
throw createParsingError(`The argument "${VARARGS_NAME}" in macro "${name}" cannot be defined because the variable "${VARARGS_NAME}" is reserved for arbitrary arguments.`, macroArgument);
throw createParsingError(`The argument "${VARARGS_NAME}" in macro "${name}" cannot be defined because the variable "${VARARGS_NAME}" is reserved for arbitrary arguments.`, macroArgument, stream.source);
}
}
@ -51,7 +51,7 @@ export const createMacroTagHandler = (): TwingTagHandler => {
if (value != name) {
const {line, column} = nextToken;
throw createParsingError(`Expected endmacro for macro "${name}" (but "${value}" given).`, {line, column}, stream.source.name);
throw createParsingError(`Expected endmacro for macro "${name}" (but "${value}" given).`, {line, column}, stream.source);
}
}

View File

@ -28,7 +28,7 @@ export const createSandboxTagHandler = (): TwingTagHandler => {
if (!(child.type === "text" && isMadeOfWhitespaceOnly(child.attributes.data))) {
if (child.type !== "include" && child.type !== "embed") {
throw createParsingError('Only "include" tags are allowed within a "sandbox" section.', child, stream.source.name);
throw createParsingError('Only "include" tags are allowed within a "sandbox" section.', child, stream.source);
}
}
}

View File

@ -24,7 +24,7 @@ export const createSetTagHandler = (): TwingTagHandler => {
if (getChildrenCount(names) !== getChildrenCount(values)) {
const {line, column} = stream.current;
throw createParsingError('When using set, you must have the same number of variables and assignments.', {line, column}, stream.source.name);
throw createParsingError('When using set, you must have the same number of variables and assignments.', {line, column}, stream.source);
}
}
else {
@ -33,7 +33,7 @@ export const createSetTagHandler = (): TwingTagHandler => {
if (getChildrenCount(names) > 1) {
const {line, column} = stream.current;
throw createParsingError('When using set with a block, you cannot have a multi-target.', {line, column}, stream.source.name);
throw createParsingError('When using set with a block, you cannot have a multi-target.', {line, column}, stream.source);
}
stream.expect("TAG_END");

View File

@ -15,7 +15,7 @@ export const createUseTagHandler = (): TwingTagHandler => {
const template = parser.parseExpression(stream);
if (template.type !== "constant") {
throw createParsingError('The template references in a "use" statement must be a string.', {line, column}, stream.source.name);
throw createParsingError('The template references in a "use" statement must be a string.', {line, column}, stream.source);
}
const targets: Record<string, TwingConstantNode<string>> = {};

View File

@ -6,9 +6,8 @@ import {TwingTemplateNode} from "./node/template";
import {mergeIterables} from "./helpers/merge-iterables";
import {createRuntimeError} from "./error/runtime";
import {createNode, getChildren, getChildrenCount, TwingBaseNode} from "./node";
import {TwingError} from "./error";
import {createMarkup, TwingMarkup} from "./markup";
import {createTemplateLoadingError, isATemplateLoadingError} from "./error/loader";
import {createTemplateLoadingError} from "./error/loader";
import {cloneMap} from "./helpers/clone-map";
import {iteratorToMap} from "./helpers/iterator-to-map";
import {TwingSource} from "./source";
@ -283,13 +282,14 @@ export const createTemplate = (
}
}
else {
// todo: use traceable method?
return Promise.reject(createTemplateLoadingError((names as Array<string | null>).map((name) => {
if (name === null) {
return '';
}
return name;
}), undefined, template.name));
})));
}
};
@ -354,10 +354,10 @@ export const createTemplate = (
if (block) {
const [blockTemplate] = block!;
throw createRuntimeError(`Block "${name}" should not call parent() in "${blockTemplate.name}" as the block does not exist in the parent template "${template.name}".`, undefined, blockTemplate.name);
throw new Error(`Block "${name}" should not call parent() in "${blockTemplate.name}" as the block does not exist in the parent template "${template.name}".`);
}
else {
throw createRuntimeError(`Block "${name}" on template "${template.name}" does not exist.`, undefined, template.name);
throw new Error(`Block "${name}" on template "${template.name}" does not exist.`);
}
}
});
@ -382,7 +382,7 @@ export const createTemplate = (
return parent.displayBlock(executionContext, name, false);
}
else {
throw createRuntimeError(`The template has no parent and no traits defining the "${name}" block.`, undefined, template.name);
throw new Error(`The template has no parent and no traits defining the "${name}" block.`);
}
});
}
@ -424,16 +424,6 @@ export const createTemplate = (
return parent.execute(environment, context, blocks, outputBuffer, options);
}
});
}).catch((error: TwingError) => {
if (!error.source) {
error.source = template.name;
}
if (isATemplateLoadingError(error)) {
error = createRuntimeError(error.rootMessage, error.location, error.source, error);
}
throw error;
});
},
getBlocks: (executionContext) => {
@ -471,9 +461,8 @@ export const createTemplate = (
const loadTemplate = getTraceableMethod(
template.loadTemplate,
parentNode.line,
parentNode.column,
template.name
parentNode,
template.source
);
const loadedParent = await loadTemplate(executionContext, parentName);
@ -501,15 +490,14 @@ export const createTemplate = (
const loadTemplate = getTraceableMethod(
template.loadTemplate,
templateNameNode.line,
templateNameNode.column,
template.name
templateNameNode,
template.source
);
const traitTemplate = await loadTemplate(executionContext, templateName);
if (!traitTemplate.canBeUsedAsATrait) {
throw createRuntimeError(`Template ${templateName} cannot be used as a trait.`, templateNameNode, template.name);
throw createRuntimeError(`Template ${templateName} cannot be used as a trait.`, templateNameNode, template.source);
}
const traitBlocks = cloneMap(await traitTemplate.getBlocks(executionContext));
@ -518,7 +506,7 @@ export const createTemplate = (
const traitBlock = traitBlocks.get(key);
if (!traitBlock) {
throw createRuntimeError(`Block "${key}" is not defined in trait "${templateName}".`, templateNameNode, template.name);
throw createRuntimeError(`Block "${key}" is not defined in trait "${templateName}".`, templateNameNode, template.source);
}
const targetValue = (target as TwingConstantNode<string>).attributes.value;

View File

@ -65,7 +65,7 @@ export const createTokenStream = (
throw createParsingError(
`${message ? message + '. ' : ''}Unexpected token "${typeToEnglish(token.type)}" of value "${token.value}" ("${typeToEnglish(type)}" expected${value ? ` with value "${value}"` : ''}).`,
{line, column},
source.name
source
);
}

View File

@ -7,7 +7,7 @@ runTest({
{{ foo.bar() }}
`
},
expectedErrorMessage: 'TwingRuntimeError: An exception has been thrown during the rendering of a template ("I am Error") in "index.twig" at line 2, column 4.',
expectedErrorMessage: 'TwingRuntimeError: I am Error in "index.twig" at line 2, column 4',
context: Promise.resolve({
foo: {
bar: () => {

View File

@ -22,7 +22,7 @@ const testCases: Array<[title: string, Record<string, any>, errorMessage: string
return 'bar';
}
})
}, 'TwingSandboxSecurityError: Calling "bar" property on an instance of (anonymous) is not allowed in "index.twig" at line 1, column 4.']
}, 'TwingRuntimeError: Calling "bar" property on an instance of (anonymous) is not allowed in "index.twig" at line 1, column 4.']
];
for (const [name, context, errorMessage] of testCases) {

View File

@ -22,7 +22,7 @@ const testCases: Array<[title: string, Record<string, any>, errorMessage: string
return 'bar';
}
})
}, 'TwingSandboxSecurityError: Calling "bar" method on an instance of (anonymous) is not allowed in "index.twig" at line 1, column 4.']
}, 'TwingRuntimeError: Calling "bar" method on an instance of (anonymous) is not allowed in "index.twig" at line 1, column 4.']
];
for (const [name, context, errorMessage] of testCases) {

View File

@ -31,17 +31,17 @@ for (const [value, expectation] of owaspTestCases) {
const specialCharacters: { [k: string]: string } = {
/* HTML special chars - escape without exception to hex */
'<': '\\3C ',
'>': '\\3E ',
'\'': '\\27 ',
'"': '\\22 ',
'&': '\\26 ',
'<': '\\u003C',
'>': '\\u003E',
'\'': '\\u0027',
'"': '\\u0022',
'&': '\\u0026',
/* Characters beyond ASCII value 255 to unicode escape */
'Ā': '\\100 ',
'Ā': '\\u0100',
/* Immune chars excluded */
',': '\\2C ',
'.': '\\2E ',
'_': '\\5F ',
',': '\\u002C',
'.': '\\u002E',
'_': '\\u005F',
/* Basic alnums excluded */
'a': 'a',
'A': 'A',
@ -50,19 +50,19 @@ const specialCharacters: { [k: string]: string } = {
'0': '0',
'9': '9',
/* Basic control characters and null */
"\r": '\\D ',
"\n": '\\A ',
"\t": '\\9 ',
"\0": '\\0 ',
"\r": '\\u000D',
"\n": '\\u000A',
"\t": '\\u0009',
"\0": '\\u0000',
/* Encode spaces for quoteless attribute protection */
' ': '\\20 ',
' ': '\\u0020',
};
for (const key in specialCharacters) {
let value = specialCharacters[key];
runTest({
description: `"escape" filter with "css" strategy on special character "${value}"`,
description: `"escape" filter with "css" strategy on special character "${key}"`,
templates: {
"index.twig": `{{ key|escape("css") }}`
},

View File

@ -24,7 +24,7 @@ class Test extends TestBase {
}
getExpectedErrorMessage() {
return 'TwingSandboxSecurityError: Filter "e" is not allowed in "foo.twig" at line 2, column 8.';
return 'TwingRuntimeError: Filter "e" is not allowed in "foo.twig" at line 2, column 8.';
}
}
@ -45,5 +45,5 @@ runTest({
allowedFunctions: ['include']
})
},
expectedErrorMessage: 'TwingSandboxSecurityError: Filter "e" is not allowed in "foo.twig" at line 2, column 8.'
expectedErrorMessage: 'TwingRuntimeError: Filter "e" is not allowed in "foo.twig" at line 2, column 8.'
});

View File

@ -20,5 +20,5 @@ runTest({
{{ random([]) }}
`
},
expectedErrorMessage: `TwingRuntimeError: The random function cannot pick from an empty array in "index.twig".`
expectedErrorMessage: `TwingRuntimeError: The random function cannot pick from an empty array in "index.twig" at line 2, column 4.`
});

View File

@ -11,7 +11,7 @@ runTest({
toString: () => 'foo'
}
}),
expectedErrorMessage: `TwingSandboxSecurityError: Tag "do" is not allowed in "index.twig" at line 1, column 4.`,
expectedErrorMessage: `TwingRuntimeError: Tag "do" is not allowed in "index.twig" at line 1, column 4.`,
sandboxed: true,
sandboxPolicy: createSandboxSecurityPolicy()
})

View File

@ -22,5 +22,5 @@ runTest({
`
},
sandboxed: true,
expectedErrorMessage: 'TwingSandboxSecurityError: Filter "upper" is not allowed in "index.twig" at line 2, column 6.'
expectedErrorMessage: 'TwingRuntimeError: Filter "upper" is not allowed in "index.twig" at line 2, column 6.'
});

View File

@ -22,5 +22,5 @@ runTest({
`
},
sandboxed: true,
expectedErrorMessage: 'TwingSandboxSecurityError: Function "dump" is not allowed in "index.twig" at line 2, column 4.'
expectedErrorMessage: 'TwingRuntimeError: Function "dump" is not allowed in "index.twig" at line 2, column 4.'
});

View File

@ -35,5 +35,5 @@ runTest({
sandboxSecurityPolicyMethods: new Map([
[Map, ['bar']]
]),
expectedErrorMessage: 'TwingSandboxSecurityError: Calling "bar" method on an instance of Object is not allowed in "index.twig" at line 2, column 4.'
expectedErrorMessage: 'TwingRuntimeError: Calling "bar" method on an instance of Object is not allowed in "index.twig" at line 2, column 4.'
});

View File

@ -35,5 +35,5 @@ runTest({
sandboxSecurityPolicyProperties: new Map([
[Map, ['bar']]
]),
expectedErrorMessage: 'TwingSandboxSecurityError: Calling "bar" property on an instance of Object is not allowed in "index.twig" at line 2, column 4.'
expectedErrorMessage: 'TwingRuntimeError: Calling "bar" property on an instance of Object is not allowed in "index.twig" at line 2, column 4.'
});

View File

@ -22,5 +22,5 @@ runTest({
`
},
sandboxed: true,
expectedErrorMessage: 'TwingSandboxSecurityError: Tag "block" is not allowed in "index.twig" at line 2, column 4.'
expectedErrorMessage: 'TwingRuntimeError: Tag "block" is not allowed in "index.twig" at line 2, column 4.'
});

View File

@ -17,7 +17,7 @@ export class Basic extends TestBase {
}
getExpectedErrorMessage() {
return 'TwingSandboxSecurityError: Calling "toString" method on an instance of Object is not allowed in "foo.twig" at line 1, column 4.';
return 'TwingRuntimeError: Calling "toString" method on an instance of Object is not allowed in "foo.twig" at line 1, column 4.';
}
getContext() {
@ -50,7 +50,7 @@ export class Set extends Basic {
}
getExpectedErrorMessage(): string {
return 'TwingSandboxSecurityError: Calling "toString" method on an instance of Object is not allowed in "foo.twig" at line 2, column 12.';
return 'TwingRuntimeError: Calling "toString" method on an instance of Object is not allowed in "foo.twig" at line 2, column 12.';
}
getSandboxSecurityPolicyTags(): string[] {

View File

@ -17,7 +17,7 @@ class Test extends TestBase {
}
getExpectedErrorMessage() {
return 'TwingSandboxSecurityError: Calling "toString" method on an instance of Object is not allowed in "foo.twig" at line 1, column 4.';
return 'TwingRuntimeError: Calling "toString" method on an instance of Object is not allowed in "foo.twig" at line 1, column 4.';
}
getSandboxSecurityPolicyFilters() {

View File

@ -17,7 +17,7 @@ class Test extends TestBase {
}
getExpectedErrorMessage() {
return 'TwingSandboxSecurityError: Calling "toString" method on an instance of Object is not allowed in "foo.twig" at line 1, column 4.';
return 'TwingRuntimeError: Calling "toString" method on an instance of Object is not allowed in "foo.twig" at line 1, column 4.';
}
getContext() {

View File

@ -18,7 +18,7 @@ class Test extends TestBase {
}
getExpectedErrorMessage() {
return 'TwingSandboxSecurityError: Filter "upper" is not allowed in "foo.twig" at line 1, column 10.';
return 'TwingRuntimeError: Filter "upper" is not allowed in "foo.twig" at line 1, column 10.';
}
}

View File

@ -18,7 +18,7 @@ class Test extends TestBase {
}
getExpectedErrorMessage() {
return 'TwingSandboxSecurityError: Function "dump" is not allowed in "foo.twig" at line 1, column 4.';
return 'TwingRuntimeError: Function "dump" is not allowed in "foo.twig" at line 1, column 4.';
}
}

View File

@ -18,7 +18,7 @@ class Test extends TestBase {
}
getExpectedErrorMessage() {
return 'TwingSandboxSecurityError: Tag "do" is not allowed in "foo.twig" at line 1, column 4.';
return 'TwingRuntimeError: Tag "do" is not allowed in "foo.twig" at line 1, column 4.';
}
}

View File

@ -20,7 +20,7 @@ class Test extends TestBase {
}
getExpectedErrorMessage(): string {
return 'TwingSandboxSecurityError: Function "range" is not allowed in "foo.twig" at line 2, column 5.';
return 'TwingRuntimeError: Function "range" is not allowed in "foo.twig" at line 2, column 5.';
}
}

View File

@ -19,5 +19,5 @@ runTest({
bar: 'foo.bar'
}
}),
expectedErrorMessage: 'TwingSandboxSecurityError: Calling "bar" property on an instance of Object is not allowed in "index.twig" at line 5, column 4.'
expectedErrorMessage: 'TwingRuntimeError: Calling "bar" property on an instance of Object is not allowed in "index.twig" at line 5, column 4.'
})

View File

@ -4,8 +4,8 @@ import * as index from "../../../src/lib";
tape('library index', ({same, end}) => {
const expected: Array<string> = [
'isATwingError',
'createTemplateLoadingError', 'isATemplateLoadingError',
'createRuntimeError', 'isARuntimeError',
'createTemplateLoadingError',
'createRuntimeError',
'createParsingError',
'createFilesystemLoader',
'createArrayLoader',

View File

@ -35,7 +35,7 @@ tape('createEnvironment ', ({test}) => {
return environment.render('foo', {})
.then(() => fail)
.catch((error: any) => {
same((error as Error).name, 'TwingTemplateLoadingError');
same((error as Error).name, 'Error');
same((error as Error).message, 'Unable to find template "foo".');
})
.finally(end);

View File

@ -2,12 +2,22 @@ import * as tape from "tape";
import {createBaseError} from "../../../../../../src/lib/error/base";
tape('createBaseError', ({test}) => {
test('creates a valid TwingRuntimeError', ({same, end}) => {
test('creates a valid TwingBaseError', ({same, end}) => {
const previousError = 'I am Error';
const error = createBaseError('name', 'message', undefined, undefined, previousError);
const error = createBaseError('name', 'message', {
column: 5,
line: 6
}, {
code: 'code',
name: 'name'
}, previousError);
same(error.previous, previousError);
same(error.rootMessage, 'message');
same(error.location.column, 5);
same(error.location.line, 6);
same(error.source.code, 'code');
same(error.source.name, 'name');
end();
})

View File

@ -1,11 +1,20 @@
import * as tape from "tape";
import {createRuntimeError, isARuntimeError} from "../../../../../../src/lib/error/runtime";
import {createRuntimeError} from "../../../../../../src/lib/error/runtime";
tape('createRuntimeError', ({test}) => {
test('creates a valid TwingRuntimeError', ({same, end}) => {
const error = createRuntimeError('foo');
const error = createRuntimeError('foo', {
column: 5,
line: 6
}, {
code: 'code',
name: 'name'
});
same(isARuntimeError(error), true);
same(error.location.column, 5);
same(error.location.line, 6);
same(error.source.code, 'code');
same(error.source.name, 'name');
end();
})

View File

@ -2,13 +2,21 @@ import * as tape from "tape";
import {TwingRuntimeError} from "../../../../../../src/lib/error/runtime";
import {createBaseNode} from "../../../../../../src/lib/node";
import {executeNode} from "../../../../../../src/lib/node-executor";
import type {TwingExecutionContext} from "../../../../../../src/lib/execution-context";
tape('executeNode => ::()', ({test}) => {
test('throws on unrecognized node type', ({same, fail, end}) => {
return executeNode(createBaseNode("foo", {}, {}, 0, 0), {} as any)
return executeNode(createBaseNode("foo", {}, {}, 0, 0), {
template: {
source: {
code: 'code',
name: 'name'
}
}
} as TwingExecutionContext)
.then(fail)
.catch((error: TwingRuntimeError) => {
same(error.message, 'Unrecognized node of type "foo" at line 0, column 0');
same(error.message, 'Unrecognized node of type "foo" in "name" at line 0, column 0');
same(error.name, 'TwingRuntimeError');
})
.finally(end);

View File

@ -3,16 +3,24 @@ import {executeBinaryNode} from "../../../../../../src/lib/node-executor/express
import {createBaseBinaryNode} from "../../../../../../src/lib/node/expression/binary";
import {createConstantNode} from "../../../../../../src/lib/node/expression/constant";
import {TwingRuntimeError} from "../../../../../../src/lib/error/runtime";
import type {TwingExecutionContext} from "../../../../../../src/lib/execution-context";
tape('executeBinaryNode => ::()', ({test}) => {
test('throws on unrecognized node type', ({same, fail, end}) => {
return executeBinaryNode(createBaseBinaryNode("foo", [
createConstantNode(0, 0, 0),
createConstantNode(0, 0, 0)
], 0, 0), {} as any)
], 0, 0), {
template: {
source: {
code: 'code',
name: 'name'
}
}
} as TwingExecutionContext)
.then(fail)
.catch((error: TwingRuntimeError) => {
same(error.message, 'Unrecognized binary node of type "foo" at line 0, column 0');
same(error.message, 'Unrecognized binary node of type "foo" in "name" at line 0, column 0');
same(error.name, 'TwingRuntimeError');
})
.finally(end);

View File

@ -3,13 +3,21 @@ import {createConstantNode} from "../../../../../../src/lib/node/expression/cons
import {TwingRuntimeError} from "../../../../../../src/lib/error/runtime";
import {executeUnaryNode} from "../../../../../../src/lib/node-executor/expression/unary";
import {createBaseUnaryNode} from "../../../../../../src/lib/node/expression/unary";
import type {TwingExecutionContext} from "../../../../../../src/lib/execution-context";
tape('executeUnaryNode', ({test}) => {
test('throws on unrecognized node type', ({same, fail, end}) => {
return executeUnaryNode(createBaseUnaryNode("foo", createConstantNode(0, 0, 0), 0, 0), {} as any)
return executeUnaryNode(createBaseUnaryNode("foo", createConstantNode(0, 0, 0), 0, 0), {
template: {
source: {
code: 'code',
name: 'name'
}
}
} as TwingExecutionContext)
.then(fail)
.catch((error: TwingRuntimeError) => {
same(error.message, 'Unrecognized unary node of type "foo" at line 0, column 0');
same(error.message, 'Unrecognized unary node of type "foo" in "name" at line 0, column 0');
same(error.name, 'TwingRuntimeError');
})

View File

@ -39,7 +39,10 @@ tape('node-traverser', (test) => {
const node = traverse(createBaseNode(null, {}, {
0: nodeToRemove,
1: nodeToKeep
}));
}), {
name: 'name',
code: 'code'
});
test.same(node?.children[0], undefined);
test.same(node?.children[1].type, 'foo');