Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Objects with null prototypes lose type information #3

Open
hildjj opened this issue Feb 6, 2021 · 3 comments
Open

Objects with null prototypes lose type information #3

hildjj opened this issue Feb 6, 2021 · 3 comments

Comments

@hildjj
Copy link
Owner

hildjj commented Feb 6, 2021

Code like this:

class Foo {}
const a = new Foo();
Object.setPrototypeOf(a, null);
inspect(a);

expected: [Foo: null prototype] {}
actual: [Object: null prototype] {}

@hildjj
Copy link
Owner Author

hildjj commented Feb 6, 2021

Similar issue:

Object.setPrototypeOf(new Number(-0), Array.prototype)

expected: [Number (Array): -0]
actual: 'Array {}'

@4nds
Copy link

4nds commented Oct 6, 2022

For the first problem, I don't think it is possible to find the name of an object's class after its prototype is set to null.

For the second issue, we can get type with Object.prototype.toString.call():

const n = new Number(-0);
Object.setPrototypeOf(n, Array.prototype);
console.log(Object.prototype.toString.call(n));
// [object Number]

The reason why we don't get expected output [Number (Array): -0] is because isBoxedPrimitive(value) returns false when check in formatRaw() function in inspect.js.

Function isBoxedPrimitive(val) is defined in internal/util/types.js:

isBoxedPrimitive(val) {
  return isNumberObject(val) ||
    isStringObject(val) ||
    isBooleanObject(val) ||
    isBigIntObject(val) ||
    isSymbolObject(val);
}

The problem is that isNumberObject(val) returns false, function isNumberObject(val) is defined with function checkBox():

function checkBox(cls) {
  return (val) => {
    if (!constructorNamed(val, cls.name)) {
      return false;
    }
    try {
      cls.prototype.valueOf.call(val);
    } catch {
      return false;
    }
    return true;
  };
}

const isNumberObject = checkBox(Number);

The problem is that constructorNamed(val, 'Number') will return false, function constructorNamed(val, ...name) is defined as:

function constructorNamed(val, ...name) {
  // Pass in names rather than types, in case SharedArrayBuffer (e.g.) isn't
  // in your browser
  for (const n of name) {
    const typ = globalThis[n];
    if (typ) {
      if (val instanceof typ) {
        return true;
      }
    }
  }
  // instanceOf doesn't work across vm boundaries, so check the whole
  // inheritance chain
  while (val) {
    if (typeof val !== 'object') {
      return false;
    }
    if (name.indexOf(getConstructorName(val)) >= 0) {
      return true;
    }
    val = Object.getPrototypeOf(val);
  }
  return false;
}

Checking with instanceof won't work in any case, but problem is that getConstructorName(val) is not in array name, function getConstructorName(val) is defined in util.js:

getConstructorName(val) {
  if (!val || typeof val !== 'object') {
    throw new Error('Invalid object');
  }
  if (val.constructor && val.constructor.name) {
    return val.constructor.name;
  }
  const str = Object.prototype.toString.call(val);
  // e.g. [object Boolean]
  const m = str.match(/^\[object ([^\]]+)\]/);
  if (m) {
    return m[1];
  }
  return 'Object';
}

And finally, the problem is that val.constructor.name is equal to 'Array' so Object.prototype.toString.call(val) is never called. To fix it, we should first check Object.prototype.toString.call(val) and then val.constructor.name. When prototype of primitive value is changed, Object.prototype.toString.call(val) will still return correct value, and when prototype of object is changed Object.prototype.toString.call(val) will either be the same as val.constructor.name or it will be '[object Object]'. So when Object.prototype.toString.call(val) is different from '[object Object]' then it returns correct value. The solution is to change getConstructorName(val) function so that it first checks Object.prototype.toString.call(val) and uses it if it is different from '[object Object]', otherwise it uses val.constructor.name:

getConstructorName(val) {
  if (!val || typeof val !== 'object') {
    throw new Error('Invalid object');
  }
  const str = Object.prototype.toString.call(val);
  // e.g. [object Boolean]
  const m = str.match(/^\[object ([^\]]+)\]/);
  if (m && m[1] !== 'Object') {
    return m[1];
  }
  if (val.constructor && val.constructor.name) {
    return val.constructor.name;
  }
  return 'Object';
}

This will fix the problem of getting correct type for primitive values with changed prototype and I don't see how it could break functionality for any other values, but I have only checked it on few examples.

@hildjj
Copy link
Owner Author

hildjj commented Oct 17, 2022

Can you submit a patch for this, please? Make sure to add yourself to a "contributors" section in package.json as well. Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants