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

Core: Update isFunction to handle unusual-@@toStringTag input #3617

Merged
merged 11 commits into from Apr 24, 2017

Conversation

gibson042
Copy link
Member

Summary

Generators and async functions are still functions (cf. ECMAScript Language Specification), and we can recognize them with typeof (but still need a guard to reject elements).

Fixes gh-3600
Fixes gh-3596

Checklist

@mention-bot
Copy link

@gibson042, thanks for your PR! By analyzing the history of the files in this pull request, we identified @markelog, @mgol and @jeresig to be potential reviewers.

doc.write( "<body onload='window.parent.iframeDone( function() {} );'>" );
doc.close();
} catch ( e ) {
window.iframeDone( function() {}, "iframes not supported" );
Copy link
Member

Choose a reason for hiding this comment

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

How this could happen?

Copy link
Member Author

Choose a reason for hiding this comment

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

The block was patterned off a similar one for isPlainObject, and is quite old as it turns out. I'll remove the try/catch.

@@ -489,8 +493,56 @@ QUnit.test( "isFunction", function( assert ) {
callme( function() {
callme( function() {} );
} );

// Functions from other windows should be matched
Globals.register( "iframeDone" );
Copy link
Member

Choose a reason for hiding this comment

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

Can we move this to separate test as well?

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 guess, but it seems to fit with the rest of these assertions pretty well. Would anyone else like to weigh in?

@@ -537,6 +539,14 @@ QUnit.test( "jQuery.Deferred.then - spec compatibility", function( assert ) {
assert.ok( true, "errors in .done callbacks don't stop .then handlers" );
} );

function faker() {
Copy link
Member

Choose a reason for hiding this comment

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

Separate test?

Copy link
Member Author

Choose a reason for hiding this comment

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

Why?

@@ -861,7 +871,19 @@ QUnit.test( "jQuery.when(nonThenable) - like Promise.resolve", function( assert
QUnit.test( "jQuery.when(thenable) - like Promise.resolve", function( assert ) {
"use strict";

var CASES = 16,
function customToStringThen() {
Copy link
Member

Choose a reason for hiding this comment

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

Separate test?

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'd really rather not; this test runs four assertions per input thenable.

src/core.js Outdated

// Support: Chrome <=57, Firefox <=52
// Don't classify callable <object>s as functions
return typeof obj === "function" && typeof obj.nodeType !== "number";
Copy link
Member

Choose a reason for hiding this comment

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

typeof obj.nodeType !== "number", what is this? Which test is related to this change?

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 preexisting. This nodeType guard protects us against what would would otherwise be a regression caused by changing from jQuery.type to typeof.

Copy link
Member

Choose a reason for hiding this comment

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

Would you mind adding this clarification comment there, right now it looks so bizarre :)

Copy link
Member

Choose a reason for hiding this comment

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

?

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 reads as Don't classify callable <object> elements as functions now; what would clarify further?

Copy link
Member

Choose a reason for hiding this comment

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

If <object> is referenced to typeof result for objects, how it could bypass out initial typeof obj === "function"?

Then function type is a subtype of object so why we can't classify function object as function?
Most of all, why we check presence of nodeType property to see if object is callable and why do we care?

"elements" part provides some clarification, but only if you know that this is the reference to the DOM elements. And even so why "element"? Why not "node"? If the logic is meant only for elements indeed then how come it cannot reach this part for other nodes? I think it also deserves some explanation as well as some examples of such an input.

I think I know answers to these questions, but after some months I might forget, you might forget, and others who going to read this might not know of course

Copy link
Member Author

@gibson042 gibson042 Apr 18, 2017

Choose a reason for hiding this comment

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

"Don't classify callable <object> elements as functions" is perfectly clear to me. Can you just tell me what you want to see here?

@markelog
Copy link
Member

Can we augment logic of jQuery.type instead? like jQuery.type(function *() {}) === 'object' right now.

@markelog
Copy link
Member

markelog commented Apr 10, 2017

On tests perspective:

In my opinion: one feature – one test, one regression – one test for regression (i.e. for bug), preferably with issue number in test description.

The simpler the better, microservices or docker containers analogies come to mind since otherwise It's really hard to support monolith and god methods with potential merge conflicts if/when we would have to revert if those constructions were already changed at the time of revert. For the same reason we no longer put HTML fixtures inside the test suite, but create them inside the test.

I think we already discuss this many times?

One issue, one pull request, independent tests, one atomic change, one commit

@timmywil
Copy link
Member

I don't think it is necessary that pull requests have one commit, but I agree with atomicity of commits. As for this particular case, I wouldn't mind rebasing all 4 of these commits into one as they all seem to address the same problem. Still, this is less important than the "how". How we address the problem is always the bigger question, and I'm wondering if we shouldn't align with Lodash here and treat generator functions as functions as well.

@gibson042
Copy link
Member Author

gibson042 commented Apr 10, 2017

jQuery.type is so vague and generic that I don't want to touch it... we only use it internally for distinguishing number/string/function/object, and even that is potentially problematic as the list of special-cased objects grows. jQuery.isFunction, on the other hand, is much more clear (but broken until this PR lands).

As for testing, I really like the efficiency of table-driven tests, and want to see us move towards them where practical, rather than away. And merge conflicts wouldn't be an issue if we set expectations from the list of cases.

@gibson042
Copy link
Member Author

Tests updated. This should be good to go.

@@ -212,7 +212,10 @@ jQuery.extend( {
noop: function() {},

isFunction: function( obj ) {
return jQuery.type( obj ) === "function";

// Support: Chrome <=57, Firefox <=52
Copy link
Member

Choose a reason for hiding this comment

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

Should we add only to both or not yet because Chrome 58 & Firefox 53 are not stable yet?

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 yet; we can update this after official releases if anyone cares.

function faker() {
assert.ok( true, "handler with non-'Function' @@toStringTag gets invoked" );
}
faker[ typeof Symbol === "function" && Symbol.toStringTag ] = "String";
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 wrap the statement with if? In the current way we'll be writing to a "false" property in non-supporting browsers which may create confusion.

These are tests so even size is not important so I don't see the gains of the current form.

onFulfilled();
}
};
promise.then[ typeof Symbol === "function" && Symbol.toStringTag ] = "String";
Copy link
Member

Choose a reason for hiding this comment

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

Similarly, I'd prefer an if instead of writing to a "false" property.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fair enough; updated.

onFulfilled();
}
};
if ( typeof Symbol === "function" ) {
Copy link
Member

Choose a reason for hiding this comment

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

This check is not enough, many browsers (e.g. old Firefox, Edge <15) support many symbols but not @@toStringTag.

This applies to here & a few other places.

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 don't think we care about those cases because they coincide with impossibility of setting @@toStringTag, but I'll see what I can do anyway.

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 decided to keep this one for its harmless property set, but the others are updated to skip tests unless Symbol.toStringTag is truthy.

@timmywil timmywil added this to the 3.3.0 milestone Apr 17, 2017
@@ -464,7 +467,7 @@ function isArrayLike( obj ) {
var length = !!obj && "length" in obj && obj.length,
type = jQuery.type( obj );

if ( type === "function" || jQuery.isWindow( obj ) ) {
if ( jQuery.isFunction( obj ) || jQuery.isWindow( obj ) ) {
Copy link
Member

Choose a reason for hiding this comment

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

Nice, did we already deprecated isWindow btw?

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

Created – #3629 :)

src/core.js Outdated

// Support: Chrome <=57, Firefox <=52
// Don't classify callable <object>s as functions
return typeof obj === "function" && typeof obj.nodeType !== "number";
Copy link
Member

Choose a reason for hiding this comment

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

?

src/core.js Outdated
return jQuery.type( obj ) === "function";

// Support: Chrome <=57, Firefox <=52
// Don't classify callable <object> elements as functions
Copy link
Member Author

Choose a reason for hiding this comment

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

@markelog ping... what do you want instead of this text?

Copy link
Member

Choose a reason for hiding this comment

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

Sorry, long week. Some in the lines of

// Browsers mentioned above will erroneously equate `typeof` 
// call on some of the DOM nodes to "function":
//
// jQuery.isFunction(document.createElement("object")) === true
//
// but not
//
// jQuery.isFunction(document.createElement("div")) !== true
//
// To circumvent it we check presense of correct `nodeType` property on
// provided argument, which all DOM nodes shoud have

@@ -439,6 +439,8 @@ QUnit.test( "isFunction", function( assert ) {
fn = function() {};
assert.ok( jQuery.isFunction( fn ), "Normal Function" );

assert.ok( !jQuery.isFunction( Object.create( fn ) ), "custom Function subclass" );

obj = document.createElement( "object" );

// Firefox says this is a function
Copy link
Member

Choose a reason for hiding this comment

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

Chrome does the same, correct?

@@ -439,6 +439,8 @@ QUnit.test( "isFunction", function( assert ) {
fn = function() {};
assert.ok( jQuery.isFunction( fn ), "Normal Function" );

assert.ok( !jQuery.isFunction( Object.create( fn ) ), "custom Function subclass" );
Copy link
Member

Choose a reason for hiding this comment

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

We can use notOk here instead? It seems more explicit that way, nitpick of course

@gibson042 gibson042 merged commit a16339b into jquery:master Apr 24, 2017
@lock lock bot locked as resolved and limited conversation to collaborators Jan 18, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
6 participants