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

Add new special assignment kinds for recognizing Object.defineProperty calls #27208

Merged
merged 7 commits into from
Oct 19, 2018

Conversation

weswigham
Copy link
Member

@weswigham weswigham commented Sep 19, 2018

As special property assignments in JS.

Fixes #25854
Fixes #26082
Fixes #6651

@sandersn
Copy link
Member

Does it also fix #26082 and #6651? (I have yet to read the code.)

Copy link
Member

@sandersn sandersn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initial comments. Most of them are nitpicky, and I haven't looked at binder or checker yet.

const special = getAssignmentDeclarationKindWorker(expr);
return special === AssignmentDeclarationKind.Property || isInJSFile(expr) ? special : AssignmentDeclarationKind.None;
}

function getAssignmentDeclarationKindWorker(expr: BinaryExpression): AssignmentDeclarationKind {
export function isBindableObjectDefinePropertyCall(expr: CallExpression): expr is BindableObjectDefinePropertyCall {
return !(length(expr.arguments) !== 3 ||
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

! ... !== .. || ? Why not write it as ... === ... && ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I thought you said the third one was optional? I guess we won't be able to get the type in that case, but we could still bind the property with type any.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not optional, it just has a few different shapes it can take.

function isReadonlySymbol(symbol: Symbol): boolean {
// The following symbols are considered read-only:
// Properties with a 'readonly' modifier
// Variables declared with 'const'
// Get accessors without matching set accessors
// Enum members
// JS Assignments with writable false or no setter
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pedantically (since I spent a day thinking about terms), it's better to say // define-property assignments with writable false or no setter. This parallels terminology of things that are parllel, like "this-property assignments" or "prototype assignments".

/** @internal */
export type BindableObjectDefinePropertyCall = CallExpression & { arguments: { 0: EntityNameExpression, 1: StringLiteralLike | NumericLiteral, 2: ObjectLiteralExpression } };


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extra newline

case SyntaxKind.PropertyAssignment:
return init.initializer;
case SyntaxKind.ShorthandPropertyAssignment:
case SyntaxKind.MethodDeclaration:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which ones are allowed to be methods? get? set? value?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Get and set often are. Value can be, it'd be the same as a property with a function expression value, though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this function is unused in my current implementation, though.

return AssignmentDeclarationKind.None;
}
const entityName = expr.arguments[0];
if ((isIdentifier(entityName) && entityName.escapedText === "exports") || (isPropertyAccessExpression(entityName) && isIdentifier(entityName.expression) && entityName.expression.escapedText === "module" && entityName.name.escapedText === "exports")) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

newline needed in here somewhere

@weswigham
Copy link
Member Author

#6651 definitely is included here. At least for JS.

@weswigham
Copy link
Member Author

I think I need extra handling to cover the prototype assignment case as in Object.defineProperty(Person.prototype for #26082.

@RyanCavanaugh RyanCavanaugh added this to the TypeScript 3.2 milestone Sep 20, 2018
@weswigham
Copy link
Member Author

@sandersn I've added support for prototype property assignments.

@sandersn sandersn self-assigned this Oct 9, 2018
case AssignmentDeclarationKind.None:
break; // Nothing to do
default:
return Debug.fail("Unknown call expression assignment declaration kind");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug.assertNever is better here I think.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assignmentKind has all the non-object-define values still "possible" (they're not, because it's a call expression and not a binary expression) and so isn't never; so not quite?

return AssignmentDeclarationKind.None;
}
const entityName = expr.arguments[0];
if ((isIdentifier(entityName) && entityName.escapedText === "exports") ||
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we should have a is[Module]Exports utility since this is repeated for the ExportsProperty case below.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hilariously enough, 10 lines up we already have one, with exactly what i was going to name it.

function getInitializerTypeFromAssignmentDeclaration(symbol: Symbol, resolvedSymbol: Symbol | undefined, expression: BinaryExpression | CallExpression, kind: AssignmentDeclarationKind) {
if (isCallExpression(expression)) {
if (resolvedSymbol) {
return getTypeOfSymbol(resolvedSymbol); // This shouldn't happen except under some hopefully forbidden merges of export assignments and object define assignments
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a test for this case?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK I can't construct something that can actually hit this codepath currently (since something like Object.defineProperty(module, "exports", { value: "no" }) won't be bound to the module in the first place); but I wanna avoid crashing and burning if we change something, and this seems reasonable (ignoring the reassignment). I'll add a test with similar content that could trigger it if we ever do start binding it a bit differently, though.

src/compiler/checker.ts Outdated Show resolved Hide resolved
isObjectLiteralExpression(container.parent.parent) &&
isCallExpression(container.parent.parent.parent) &&
container.parent.parent.parent.arguments[2] === container.parent.parent &&
getAssignmentDeclarationKind(container.parent.parent.parent) === AssignmentDeclarationKind.ObjectDefinePrototypeProperty) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(this applies to all the cases in this function) Would it be possible, and more efficient, to just check that parent x 3 exists and then just check the result of getAssignmentDeclarationKind? I think the rest of the predicate is duplicated with it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite - these are actually some extra constraints which ask that the third argument be specifically an object literal (rather than anything we might be able to pull a type from), to inject a this type into the method defined in it, even if Object.defineProperty's signature lacks a strong this (and, in fact, it does lack one).

case AssignmentDeclarationKind.ObjectDefinePropertyValue:
case AssignmentDeclarationKind.ObjectDefinePropertyExports:
case AssignmentDeclarationKind.ObjectDefinePrototypeProperty:
return Debug.fail("Unimplemented");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Not yet implemented" or "Does not apply"? Isn't it the latter?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, definitely the later.

src/compiler/checker.ts Outdated Show resolved Hide resolved
Copy link
Member

@sandersn sandersn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some questions and suggestions, plus I need to look at the tests.

@sandersn
Copy link
Member

Can you see what happens with the user tests? (and maybe RWC, although I don't think there's much JS there.)


==== tests/cases/conformance/jsdoc/index.js (0 errors) ====
const x = {};
Object.defineProperty(x, "name", { value: "Charles", writable: true });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should have a value test that leaves off writable, if that's allowed. Actually, you should have a test with value left out too. And maybe a test with some of the properties we don't care about?

Also a test with non-literals, just in case that hurts our ... error reporting or something?

According to MDN's Object.defineProperty page, we should have an error if both get/set and value are present. I didn't see a test for that either.

Also, who spells 'writeable' as 'writable'?
Later:everybody else, it turns out.

@weswigham
Copy link
Member Author

@sandersn I've replied to or addressed all of your comments; how'd you feel about this now?

@sandersn
Copy link
Member

sandersn commented Oct 11, 2018

Looks good except for four things:

  1. A test that has an initialiser that is just a variable, not some literal.
  2. A test that has a property name that is a variable of type string. (It shouldn’t result in a property, right?)
  3. A test that has a property name that is a variable with constant value, if the behaviour is different than 2.
  4. An error for when you have both value and get+set, or neither.

@weswigham
Copy link
Member Author

An error for when you have both value and get+set, or neither

The function is still typechecked as a function call, so that aughta still be happening provided the lib definition is written such that the argument is a union (it isn't, it uses a single weak type).

@sandersn
Copy link
Member

OK, then I guess we should consider changing it afterward. It would probably break some code, though.

@sandersn
Copy link
Member

@weswigham It would be nice to get this one in. Will you have time to add those tests some time?

@weswigham
Copy link
Member Author

Yeah, I'll take some time to add them today.

@weswigham
Copy link
Member Author

@sandersn done

@weswigham weswigham merged commit 69b1cb5 into microsoft:master Oct 19, 2018
@weswigham weswigham deleted the special-object-assignment branch October 19, 2018 21:32
@quantizor
Copy link

Sorry to comment on a long closed issue, but is this also implemented for Object.defineProperties? https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperties

@weswigham
Copy link
Member Author

Nope.

@calvintwr
Copy link

Does this still work? It's not for me in VScode:

Version: 1.59.0
Commit: 379476f0e13988d90fab105c5c19e7abc8b1dea8
Date: 2021-08-04T23:14:40.191Z
Electron: 13.1.7
Chrome: 91.0.4472.124
Node.js: 14.16.0
V8: 9.1.269.36-electron.0
OS: Darwin x64 19.6.0

Suppose the following code:

test.ts:

const something = {
    somethingValue: 'test'
}
Object.defineProperty(something, 'definedProperty', {
    value: 'hello'
})
export default something

test2.ts:

import Thing from './test'

Thing.somethingValue // this is okay
Thing.definedProperty // this will have red curly underline

So why does Thing.definedProperty have red curlies? @weswigham @sandersn

@weswigham
Copy link
Member Author

We only recognize them in .js files, not .ts.

@calvintwr
Copy link

calvintwr commented Aug 15, 2021

I see. But because it wasn't working in JS, that's why I converted it to TS to see if it works.

I reverted the files to JS, and the screenshot below shows that it wasn't able to see the property defined using #defineProperty:

Screenshot 2021-08-16 at 1 33 19 AM

@enricoangelon
Copy link

We only recognize them in .js files, not .ts.

I think it's time for a .ts implementation too!

@AR0x7E7
Copy link

AR0x7E7 commented Apr 2, 2022

using Visual Studio Code 1.66.0 I can confirm that Object.defineProperty appears to be not recognized
Unbenannt

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