Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

feat($parse): enable optional access to the underlying AST #16260

Closed
wants to merge 1 commit into from
Closed

feat($parse): enable optional access to the underlying AST #16260

wants to merge 1 commit into from

Conversation

fpipita
Copy link
Contributor

@fpipita fpipita commented Oct 6, 2017

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

Copy link
Contributor

@petebacondarwin petebacondarwin left a 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.

Copy link
Member

@gkalpak gkalpak left a 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
Copy link
Member

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.
Copy link
Member

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;
}
};
Copy link
Member

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);
Copy link
Member

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;
Copy link
Member

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).

it('should be forbidden by default', inject(function($parse) {
var fn = $parse('foo.bar');
expect(fn.$$ast).toBeUndefined();
}));
Copy link
Member

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: '...'})?

it('should be configurable on a per call basis', inject(function($parse) {
var fn = $parse({expression: 'foo.bar', $$exposeAst: true});
expect(fn.$$ast).toBeDefined();
}));
Copy link
Member

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;
};

/**
Copy link
Member

Choose a reason for hiding this comment

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

Indentation isssue 😱 😱 😱 😱 😱 😱 😱

@petebacondarwin
Copy link
Contributor

Thinking more about this...

Would it be simpler just to expose a method $parse.$$getAst(expression) instead? The lexer and ast builder will already be set up; and this would avoid all of these extra provider flags and mucking about with the parameters of a public API service.

@fpipita
Copy link
Contributor Author

fpipita commented Oct 6, 2017

@petebacondarwin your proposal looks actually cleaner. I'll rework the PR and follow the new approach.

@fpipita
Copy link
Contributor Author

fpipita commented Oct 7, 2017

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.

@Narretz Narretz added this to the Backlog milestone Oct 9, 2017
Copy link
Contributor

@petebacondarwin petebacondarwin left a 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') {
Copy link
Member

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.

Copy link
Contributor Author

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.

@gkalpak
Copy link
Member

gkalpak commented Oct 9, 2017

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?

@petebacondarwin
Copy link
Contributor

@gkalpak see #16253 (comment)

@gkalpak
Copy link
Member

gkalpak commented Oct 10, 2017

Yes, I've seen that. I am just curious what are they going to do with the AST?

@fpipita
Copy link
Contributor Author

fpipita commented Oct 10, 2017

@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:

  1. It abstracts away the underlaying ui framework (I'm using Bootstrap) by taking care of generating the required markup;
  2. It takes care of adding a default label for the input field by using the convention that the label is named the same way as the _.startCase'd name of identifier ng-model is bound to, so in the above example, the input field will get a default label of "Description";
  3. It also adds the name attribute to the input field which by convention is exactly the same as the identifier's one;

It works in collaboration with an overloaded version of ng-model, which does the job of informing the cmFormField's controller about the identifier's name.

To extract the identifier's name, ng-model needs to parse the expression it is bound to and, at the moment, I'm achieving it by using a regular expression. It works fairly well for simple use cases, but if I wanted to do anything more complex (like detecting a computed property's name), I fear things can get quickly messy :)

<cm-form-field>
  <input ng-model="foo[$ctrl.bar]" />
</cm-form-field>

By using the AST, it would be easy to do it, instead.

@gkalpak
Copy link
Member

gkalpak commented Oct 10, 2017

@fpipita, I see. Pretty interesting usecase. Thx for sharing 😃

@fpipita
Copy link
Contributor Author

fpipita commented Nov 19, 2017

@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 exp.trim() call, which is still done twice, in both $parse and in the newly added Parser.prototype.getAst.

Copy link
Member

@gkalpak gkalpak left a 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).

@fpipita
Copy link
Contributor Author

fpipita commented Nov 27, 2017

@gkalpak I've updated the PR description, thank you for pointing that out.

Do you think that the refactor tag would be appropriate for the type of changes? If not, which one would you suggest to use?

One last thing .. will you please let me know whether I can merge the two commits? :)

@petebacondarwin
Copy link
Contributor

Please do squash the two commits.

I think it is OK to include it as a feat commit. As long as the description makes it clear that it is a private feature and that people should not rely on it being stable. (There is little point in hiding it, since it is simple to just read through the commits if one really wants).

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
@fpipita
Copy link
Contributor Author

fpipita commented Nov 28, 2017

Hi, I've squashed the two commits and tried to make it clearer through the description that this is a private feature.

@gkalpak
Copy link
Member

gkalpak commented Nov 28, 2017

Still LGTM (restarted Travis - the failures looked like flakes)

@gkalpak gkalpak closed this in 2e03aed Nov 30, 2017
gkalpak pushed a commit that referenced this pull request Nov 30, 2017
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

Closes #16260
@fpipita fpipita deleted the ast branch May 17, 2018 09:22
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Proposal: expose the AST for expressions parsed through $parse.
5 participants