Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Make object or class property only invocable

TL:DR; Is it possible to make a property of object to be invocable ( as a function ) only ?

What i mean by this

class Foo{
  bar(value){
    return value
  }
}

let newFoo = new Foo()

console.log(newFoo.bar(123))  // should work fine as function is invoked
console.log(newFoo.bar)  // here i need to throw or display an error instead of returning value

I tried to do this with Proxy and handler.get trap, but i have no clue how to capture whether it is a function call or just property access,

class Foo {
  bar(value) {
    return value
  }
}


const proxied = new Proxy(new Foo(), {
  get: function(target, prop, reciver) {
    if (prop === 'bar') {
      throw new Error('Bar is method need to be invoced')
    }
    return target[prop]
  }
})

console.log(proxied.bar(true))
console.log(proxied.bar)

I have also checked handler.apply but this also doesn't seems to be of no use as this is a trap on function, not on property

class Foo {
  bar(value) {
    return value
  }
}


const proxied = new Proxy(new Foo(), {
  apply: function(target, thisArg, argumentsList) {
    return target(argumentsList[0])
  },
  get: function(target, prop, reciver) {
    if (prop === 'bar') {
      throw new Error('Bar is method need to be invoced')
    }
    return target[prop]
  }
})

console.log(proxied.bar(true))
console.log(proxied.bar)
like image 499
Code Maniac Avatar asked Sep 21 '19 07:09

Code Maniac


1 Answers

No, this is not possible. There is no distinction between

const newFoo = new Foo()
newFoo.bar(123);

and

const newFoo = new Foo()
const bar = newFoo.bar;
Function.prototype.call.call(bar, newFoo, 123); // like `bar.call(newFoo, 123)`
// or Reflect.apply(bar, newFoo, [123]);

i.e. neither newFoo nor bar can distinguish these "from the inside". Now arbitrary things could happen in between the property access and the method call, and during the property access you cannot know what will happen next, so you cannot throw an exception prematurely. The method call might happen never (in newFoo.bar;), and there's no way to recognise that from newFoo alone.

The only approach would be to intercept all other accesses on newFoo and its properties, and throw after you detected a mischievous sequence; possibly having your "linter" check the sequence from the outside after the whole program ran:

const lint = {
  access: 0,
  call: 0,
  check() {
    console.log(this.access == this.call
      ? "It's ok"
      : this.access > this.call
        ? "method was not called"
        : "property was reused");
  },
  run(fn) {
    this.call = this.access = 0;
    try {
      fn();
    } finally {
      this.check();
    }
  }
}

function bar(value) {
  lint.call++; lint.check();
  return value;
}
class Foo {
  get bar() {
    lint.check(); lint.access++;
    return bar;
  }
}
lint.run(() => {
  const newFoo = new Foo;
  newFoo.bar(123);
});
lint.run(() => {
  const newFoo = new Foo;
  newFoo.bar;
});
lint.run(() => {
  const newFoo = new Foo;
  const bar = newFoo.bar;
  bar(123);
  bar(456);
});

The better solution would probably to write your own interpreter for simple expressions, which would only allow method calls.

like image 126
Bergi Avatar answered Oct 16 '22 20:10

Bergi