-
Notifications
You must be signed in to change notification settings - Fork 27.5k
feat($parse): enable optional access to the underlying AST #16260
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be worth mentioning the potential memory issues with this in the documentation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good overall 👍 I left a couple of (mostly minor) comments.
src/ng/parse.js
Outdated
@@ -1763,6 +1768,24 @@ function $ParseProvider() { | |||
return this; | |||
}; | |||
|
|||
/** | |||
* @ngdoc method |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is supposed to be private API, we don't want it to appear in the public docs.
src/ng/parse.js
Outdated
* This feature is not meant to be part of the public API, so use it at your own risk. | ||
* | ||
* @param {boolean} exposeAst if set to true, the AST of each parsed expression will be exposed through | ||
* an `$$ast` property added to the $parse's return value. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the $parse's return value --> `$parse`'s return value
src/ng/parse.js
Outdated
if (isBoolean(exposeAst)) { | ||
globalExposeAst = exposeAst; | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be better to stick to the getter/setter model we use in other places:
this.$$exposeAst = function(exposeAst) {
if (isUndefined(exposeAst)) {
// Getter
return globalExposeAst;
} else {
// Setter
globalExposeAst = !!exposeAst; // or: (exposeAst === true);
return this;
}
}
src/ng/parse.js
Outdated
@@ -1790,7 +1821,7 @@ function $ParseProvider() { | |||
} | |||
var lexer = new Lexer($parseOptions); | |||
var parser = new Parser(lexer, $filter, $parseOptions); | |||
parsedExpression = parser.parse(exp); | |||
parsedExpression = parser.parse(exp, finalExposeAst); | |||
parsedExpression.oneTime = !!oneTime; | |||
|
|||
cache[cacheKey] = addWatchDelegate(parsedExpression); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One problem is that cached parsedExpressions will always follow the $$exposeAst
from the first call. E.g.:
$parse('user.name'); // --> No `$$ast` (expected)
$parse({expression: 'user.name', $$exposeAst: true}); // --> No `$$ast` (ooops)
Not sure what is the best way to handle that (without impacting uninterested users) 😕
Also, theoretically, the addInterceptor()
function should preserve the $$ast
property, but we can assume nobody using $$exposeAst
cares about interceptors at this point. Maybe at least add a comment somewhere (that adding an interceptor won't preserve the exposed $$ast
(if any)).
src/ng/parse.js
Outdated
@@ -1775,6 +1798,14 @@ function $ParseProvider() { | |||
|
|||
function $parse(exp, interceptorFn) { | |||
var parsedExpression, oneTime, cacheKey; | |||
var finalExposeAst = globalExposeAst; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The final
prefix is kind of confusing imo. I would just call it exposeAst
(or localExposeAst
).
test/ng/parseSpec.js
Outdated
it('should be forbidden by default', inject(function($parse) { | ||
var fn = $parse('foo.bar'); | ||
expect(fn.$$ast).toBeUndefined(); | ||
})); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you also add a test with $parse({expression: '...'})
?
test/ng/parseSpec.js
Outdated
it('should be configurable on a per call basis', inject(function($parse) { | ||
var fn = $parse({expression: 'foo.bar', $$exposeAst: true}); | ||
expect(fn.$$ast).toBeDefined(); | ||
})); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you also add a test with $$exposeAst: false
?
src/ng/parse.js
Outdated
@@ -1763,6 +1768,24 @@ function $ParseProvider() { | |||
return this; | |||
}; | |||
|
|||
/** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indentation isssue 😱 😱 😱 😱 😱 😱 😱
Thinking more about this... Would it be simpler just to expose a method |
@petebacondarwin your proposal looks actually cleaner. I'll rework the PR and follow the new approach. |
Hi, I've reworked the PR and implemented the new behavior. I didn't fixup the commits yet to prevent the comments by @gkalpak from being removed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is much better and cleaner from an API point of view. I am frustrated by the code duplication between the new $$getAst
function and the $parse
function.
I think that we can get the code size and complexity down a bit by refactoring the Parser
to expose a new function getAst()
. The cost of creating a parser is pretty small (it has to create an ASTCompiler/ASTInterpreter
but their constructors are very simple).
Moreover we could move the parsing of the one-time binding flag (::
) into a helper in the Parser
which might save some more lines.
But these are internal refactorings that I would be happy to land in subsequent PRs.
src/ng/parse.js
Outdated
} | ||
|
||
function isOneTimeBinding(exp) { | ||
if (typeof exp !== 'string') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need this check afaict.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added this check to future-proof this utility function, just in case we wanted to use it in contexts where it is not safe to assume that the caller will pass it a string.
If we can assume that it'll always be called with a string then I'll remove the check.
I just realized I have no idea how you plan to use the returned AST. Can you give an example of how you would use this feature? |
Yes, I've seen that. I am just curious what are they going to do with the AST? |
@petebacondarwin thank you for your feedback, I can further improve the PR with the refactorings. That duplication made me quite frustrated as well! @gkalpak sure, I'll share what I'm doing :) I've built a form field component which can be used like: <cm-form-field>
<!-- it can be anything bound to a model through ng-model -->
<input ng-model="foo.bar.description" />
</cm-form-field> The component has three main responsibilities:
It works in collaboration with an overloaded version of To extract the identifier's name, <cm-form-field>
<input ng-model="foo[$ctrl.bar]" />
</cm-form-field> By using the AST, it would be easy to do it, instead. |
@fpipita, I see. Pretty interesting usecase. Thx for sharing 😃 |
@petebacondarwin I've reworked a bit the PR in order to remove the code duplication and I've added the changes as a distinct commit so it is easier to inspect them and eventually discard them. I've also removed the two initial commits related to the first implementation, since everyone agreed about it looking quite bad. I'm almost entirely happy with how it looks right now, except for the only bit I wasn't able to deduplicate, the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM (although this PR description needs updating).
I wonder if we want to show this commit on the changelog (considering it is a "non-public" feature).
@gkalpak I've updated the PR description, thank you for pointing that out. Do you think that the One last thing .. will you please let me know whether I can merge the two commits? :) |
Please do squash the two commits. I think it is OK to include it as a |
This PR adds a new private method to the `$parse` service, `$$getAst`, which takes an Angular expression as its only argument and returns the computed AST. This feature is not meant to be part of the public API and might be subject to changes, so use it with caution. Closes #16253
Hi, I've squashed the two commits and tried to make it clearer through the description that this is a private feature. |
Still LGTM (restarted Travis - the failures looked like flakes) |
This PR adds a new private method to the
$parse
service,$$getAst
,which takes an Angular expression as its only argument and returns
the computed AST. This feature is not meant to be part of the public
API and might be subject to changes, so use it with caution.
Closes #16253
What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)
Feature.
What is the current behavior? (You can also link to an open issue here)
#16253
What is the new behavior (if this is a feature change)?
The new opt-in behavior introduces a way to expose the ast.
Does this PR introduce a breaking change?
No.
Please check if the PR fulfills these requirements