Skip to content

Commit c731380

Browse files
committed
fix: narrow isClass fallback to exclude native constructors
The prototype-based fallback incorrectly treated native constructors (Object, Array, Function, etc.) as user-defined classes because they also have non-writable prototype. Add a [native code] check to the fallback condition to skip these. This caused CI failures in validation/validation-zod tests because MetadataManager.formatTarget() would normalize targets incorrectly, leading to lost metadata on PickDto/OmitDto. Also use jest.isolateModules in tests so the toString mock takes effect after module-level caching.
1 parent 01c0fa7 commit c731380

2 files changed

Lines changed: 31 additions & 11 deletions

File tree

packages/core/src/util/types.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,20 @@ export function isClass(fn) {
1313
return false;
1414
}
1515

16-
if (/^class[\s{]/.test(ToString.call(fn))) {
16+
const fnSource = ToString.call(fn);
17+
if (/^class[\s{]/.test(fnSource)) {
1718
return true;
1819
}
1920

2021
// Tools like Bytenode replace function source text, so Function#toString is
2122
// unreliable. ECMAScript class constructors have a non-writable `prototype`.
23+
// Native constructors also satisfy this shape, so skip "[native code]" here.
2224
const descriptor = Object.getOwnPropertyDescriptor(fn, 'prototype');
23-
if (descriptor && descriptor.writable === false) {
25+
if (
26+
descriptor &&
27+
descriptor.writable === false &&
28+
!/\[native code\]/.test(fnSource)
29+
) {
2430
return true;
2531
}
2632

packages/core/test/decorator/util/index.test.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,27 +24,41 @@ describe('/test/util/index.test.ts', () => {
2424
expect(Types.isString(undefined)).toBeFalsy();
2525
expect(Types.isString({})).toBeFalsy();
2626

27-
expect(Types.isClass(class B {})).toBeTruthy();
27+
function plainFn() {}
28+
const asyncFn = async function () {};
29+
expect(Types.isClass(plainFn)).toBeFalsy();
30+
expect(Types.isClass(asyncFn)).toBeFalsy();
31+
expect(Types.isClass(() => {})).toBeFalsy();
32+
expect(Types.isClass(Object)).toBeFalsy();
33+
expect(Types.isClass(Array)).toBeFalsy();
34+
expect(Types.isClass(Function)).toBeFalsy();
35+
});
2836

37+
it('should detect class when toString source is obscured', () => {
2938
class ObscuredCtor {}
3039
const origToString = Function.prototype.toString;
3140
try {
32-
Function.prototype.toString = function (this: unknown) {
41+
jest.resetModules();
42+
Function.prototype.toString = function () {
3343
if (this === ObscuredCtor) {
3444
return 'function () {}';
3545
}
3646
return origToString.call(this);
3747
};
38-
expect(Types.isClass(ObscuredCtor)).toBeTruthy();
48+
49+
let isolatedTypes!: typeof Types;
50+
jest.isolateModules(() => {
51+
isolatedTypes = require('../../../src/util/types').Types;
52+
});
53+
54+
expect(isolatedTypes.isClass(ObscuredCtor)).toBeTruthy();
55+
expect(isolatedTypes.isClass(Object)).toBeFalsy();
56+
expect(isolatedTypes.isClass(Array)).toBeFalsy();
57+
expect(isolatedTypes.isClass(Function)).toBeFalsy();
3958
} finally {
4059
Function.prototype.toString = origToString;
60+
jest.resetModules();
4161
}
42-
43-
function plainFn() {}
44-
const asyncFn = async function () {};
45-
expect(Types.isClass(plainFn)).toBeFalsy();
46-
expect(Types.isClass(asyncFn)).toBeFalsy();
47-
expect(Types.isClass(() => {})).toBeFalsy();
4862
});
4963

5064
it('should test toAsyncFunction', async () => {

0 commit comments

Comments
 (0)