diff --git a/src/modules/helpers/eval.ts b/src/modules/helpers/eval.ts index 663aa334df6..a19f0eaf534 100644 --- a/src/modules/helpers/eval.ts +++ b/src/modules/helpers/eval.ts @@ -44,8 +44,11 @@ const REGEX_DOT_OR_BRACKET = /\.|\(/; * const airlineCode = fakeEval('airline.airline.iataCode', faker); // 'EY' * const airlineName = fakeEval('airline.airline().name', faker); // 'Etihad Airways' * const airlineMethodName = fakeEval('airline.airline.name', faker); // 'bound airline' + * const isoDate = fakeEval('date.anytime.toISOString', faker); // '2025-01-01T00:00:00.000Z' * ``` * + * Properties and prototype methods on generated values can also be resolved. + * * It is not possible to execute arbitrary JavaScript through this method; * expressions can only resolve properties and methods reachable from the given entrypoints. * @@ -61,6 +64,7 @@ const REGEX_DOT_OR_BRACKET = /\.|\(/; * fakeEval('person.lastName', faker) // 'Barrows' * fakeEval('helpers.arrayElement(["heads", "tails"])', faker) // 'tails' * fakeEval('number.int(9999)', faker) // 4834 + * fakeEval('date.anytime.toISOString', faker) // '2025-01-01T00:00:00.000Z' * * @since 8.4.0 */ @@ -221,8 +225,12 @@ function resolveProperty(entrypoint: unknown, key: string): unknown { return resolveProperty(entrypoint, key); } - case 'object': { - return entrypoint?.[key as keyof typeof entrypoint]; + case 'bigint': + case 'boolean': + case 'number': + case 'object': + case 'string': { + return getProperty(entrypoint, key); } default: { @@ -230,3 +238,22 @@ function resolveProperty(entrypoint: unknown, key: string): unknown { } } } + +type Callable = (this: unknown, ...args: never[]) => unknown; + +/** + * Resolves the given property and binds methods to their source value. + * + * @param entrypoint The entrypoint to resolve the property on. + * @param key The property name to resolve. + */ +function getProperty(entrypoint: unknown, key: string): unknown { + if (entrypoint == null) { + return undefined; + } + + const value = (new Object(entrypoint) as Record)[key]; + return typeof value === 'function' + ? (value as Callable).bind(entrypoint) + : value; +} diff --git a/src/modules/helpers/module.ts b/src/modules/helpers/module.ts index e30b00f0b1c..29c0d19d677 100644 --- a/src/modules/helpers/module.ts +++ b/src/modules/helpers/module.ts @@ -1197,6 +1197,13 @@ export class HelpersModule extends SimpleHelpersModule { * const message = faker.helpers.fake('Your pin is {{string.numeric(4, {"allowLeadingZeros": true})}}.'); * ``` * + * Properties and prototype methods on generated values can also be resolved: + * + * ```js + * const airlineCode = faker.helpers.fake('{{airline.airline.iataCode}}'); // 'AA' + * const timestamp = faker.helpers.fake('{{date.anytime.toISOString}}'); // '2025-01-01T00:00:00.000Z' + * ``` + * * The pattern is not evaluated as JavaScript: only faker methods can be called, and any * parameters are parsed as JSON or plain strings. Nevertheless, it is possible for certain * maliciously crafted patterns to use large amounts of memory or CPU time, so the pattern @@ -1215,6 +1222,7 @@ export class HelpersModule extends SimpleHelpersModule { * faker.helpers.fake('You can visit me at {{location.streetAddress(true)}}.') // 'You can visit me at 3393 Ronny Way Apt. 742.' * faker.helpers.fake('I flipped the coin and got: {{helpers.arrayElement(["heads", "tails"])}}') // 'I flipped the coin and got: tails' * faker.helpers.fake('Your PIN number is: {{string.numeric(4, {"exclude": ["0"]})}}') // 'Your PIN number is: 4834' + * faker.helpers.fake('Expires on {{date.future.toISOString.substring(0,10)}}') // 'Expires on 2026-09-09' * * @since 7.4.0 */ @@ -1253,6 +1261,13 @@ export class HelpersModule extends SimpleHelpersModule { * const message = faker.helpers.fake(['Your pin is {{string.numeric(4, {"allowLeadingZeros": true})}}.']); * ``` * + * Properties and prototype methods on generated values can also be resolved: + * + * ```js + * const airlineCode = faker.helpers.fake(['{{airline.airline.iataCode}}']); // 'AA' + * const timestamp = faker.helpers.fake(['{{date.anytime.toISOString}}']); // '2025-01-01T00:00:00.000Z' + * ``` + * * The pattern is not evaluated as JavaScript: only faker methods can be called, and any * parameters are parsed as JSON or plain strings. Nevertheless, it is possible for certain * maliciously crafted patterns to use large amounts of memory or CPU time, so the pattern @@ -1265,6 +1280,8 @@ export class HelpersModule extends SimpleHelpersModule { * * @example * faker.helpers.fake(['A: {{person.firstName}}', 'B: {{person.lastName}}']) // 'A: Barry' + * faker.helpers.fake(['Your PIN number is: {{string.numeric(4, {"exclude": ["0"]})}}']) // 'Your PIN number is: 4834' + * faker.helpers.fake(['Expires on {{date.future.toISOString.substring(0,10)}}']) // 'Expires on 2026-09-09' * * @since 8.0.0 */ @@ -1300,6 +1317,13 @@ export class HelpersModule extends SimpleHelpersModule { * const message = faker.helpers.fake('Your pin is {{string.numeric(4, {"allowLeadingZeros": true})}}.'); * ``` * + * Properties and prototype methods on generated values can also be resolved: + * + * ```js + * const airlineCode = faker.helpers.fake('{{airline.airline.iataCode}}'); // 'AA' + * const timestamp = faker.helpers.fake('{{date.anytime.toISOString}}'); // '2025-01-01T00:00:00.000Z' + * ``` + * * The pattern is not evaluated as JavaScript: only faker methods can be called, and any * parameters are parsed as JSON or plain strings. Nevertheless, it is possible for certain * maliciously crafted patterns to use large amounts of memory or CPU time, so the pattern @@ -1318,6 +1342,8 @@ export class HelpersModule extends SimpleHelpersModule { * faker.helpers.fake('You can visit me at {{location.streetAddress(true)}}.') // 'You can visit me at 3393 Ronny Way Apt. 742.' * faker.helpers.fake('I flipped the coin and got: {{helpers.arrayElement(["heads", "tails"])}}') // 'I flipped the coin and got: tails' * faker.helpers.fake(['A: {{person.firstName}}', 'B: {{person.lastName}}']) // 'A: Barry' + * faker.helpers.fake('Your PIN number is: {{string.numeric(4, {"exclude": ["0"]})}}') // 'Your PIN number is: 4834' + * faker.helpers.fake(['Expires on {{date.future.toISOString.substring(0,10)}}']) // 'Expires on 2026-09-09' * * @since 7.4.0 */ diff --git a/test/modules/helpers-eval.spec.ts b/test/modules/helpers-eval.spec.ts index 538ee4f36a0..6b9d0594f2b 100644 --- a/test/modules/helpers-eval.spec.ts +++ b/test/modules/helpers-eval.spec.ts @@ -131,6 +131,29 @@ describe('fakeEval()', () => { ).toContain(actual); }); + it('supports properties and prototype methods after generated values', () => { + const entrypoint = { + date: () => new Date('2025-01-01T00:00:00.000Z'), + number: () => 1234, + string: () => ' value ', + }; + + expect(fakeEval('number.toPrecision(3)', faker, [entrypoint])).toBe( + '1.23e+3' + ); + expect(fakeEval('number().toPrecision(3)', faker, [entrypoint])).toBe( + '1.23e+3' + ); + expect(fakeEval('string.trim', faker, [entrypoint])).toBe('value'); + expect(fakeEval('string().length', faker, [entrypoint])).toBe(9); + expect(fakeEval('date.toISOString', faker, [entrypoint])).toBe( + '2025-01-01T00:00:00.000Z' + ); + expect(fakeEval('date().toISOString', faker, [entrypoint])).toBe( + '2025-01-01T00:00:00.000Z' + ); + }); + it('requires a dot after a function call', () => { expect(() => fakeEval('airline.airline()iataCode', faker)).toThrow( new FakerError( @@ -187,6 +210,7 @@ describe('fakeEval()', () => { 'do.evil.constructor.constructor(globalThis.__fakerPwned = true)', 'do.evil.constructor.constructor(globalThis.__fakerPwned = true)()', 'string.constructor.constructor(globalThis.__fakerPwned = true)()', + 'string.alpha(1).constructor.constructor(globalThis.__fakerPwned = true)()', ]; beforeEach(() => { diff --git a/test/modules/helpers.spec.ts b/test/modules/helpers.spec.ts index 97ec72acfd1..09e4fa650e4 100644 --- a/test/modules/helpers.spec.ts +++ b/test/modules/helpers.spec.ts @@ -1077,6 +1077,18 @@ describe('helpers', () => { expect(faker.helpers.fake('{{string.alphanumeric(0)}}')).toBe(''); }); + it('should resolve properties after generated primitive values', () => { + expect(faker.helpers.fake('{{string.alpha(10).length}}')).toBe('10'); + }); + + it('should call prototype methods with their generated receiver', () => { + expect( + faker.helpers.fake( + '{{date.anytime({"refDate":"2025-01-01T00:00:00.000Z"}).toISOString}}' + ) + ).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); + }); + it('should be able to return locale definition strings', () => { expect(faker.definitions.cell_phone?.formats).toContain( faker.helpers.fake('{{cell_phone.formats}}')