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

whitelist of variables #4

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open

whitelist of variables #4

wants to merge 7 commits into from

Conversation

hamousavi
Copy link
Owner

If a whitelist of variables is provided using meta tags for example as follows
<meta name="amp-variable-substitution-whitelist" content="RANDOM,TIMEZONE">
then the runtime should respect it. In order to do this we parse the meta tag in GlobalVariableSource.

@hamousavi
Copy link
Owner Author

@raywainman @zhangsu

@@ -109,7 +109,10 @@ export function getNavigationData(win, attribute) {
* and override initialize() to add more supported variables.
*/
export class VariableSource {
constructor() {
constructor(ampdoc) {
/** @const {!./ampdoc-impl.AmpDoc} */
Copy link

Choose a reason for hiding this comment

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

@protected

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done.

@@ -109,7 +109,10 @@ export function getNavigationData(win, attribute) {
* and override initialize() to add more supported variables.
*/
export class VariableSource {
constructor() {
constructor(ampdoc) {
Copy link

Choose a reason for hiding this comment

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

Since the constructor is no longer nullary, it needs type annotation.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done.

@@ -121,6 +124,9 @@ export class VariableSource {

/** @private {boolean} */
this.initialized_ = false;

/** @const @private {?Array<string>} */
this.ampVariableSubstitutionWhitelist_ = this.createWhitelist_();
Copy link

Choose a reason for hiding this comment

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

The method is named in a way to communicate that it creates a "whitelist", but the field is named ampVariableSubstitutionWhitelist_. Can we be consistent?

I find words like amp and substitution a bit redundant here. How about this:

this.whitelist_ = this.createWhitelist_();

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done.

* (if provided in a meta tag).
* @private
*/
createWhitelist_() {
Copy link

Choose a reason for hiding this comment

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

Consider moving this to just below the constructor, since it only exists to support the constructor (footnote style).

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done.

@@ -164,6 +170,11 @@ export class VariableSource {
*/
set(varName, syncResolver) {
dev().assert(varName.indexOf('RETURN') == -1);
if (this.ampVariableSubstitutionWhitelist_ &&
!this.ampVariableSubstitutionWhitelist_.includes(varName)) {
Copy link

Choose a reason for hiding this comment

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

Given that the binary boolean operation here is duplicated, consider moving this to a private helper: if (this.isWhitelisted_(varName)).

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done.

}

const head = this.ampdoc.getRootNode().head;
if (head) {
Copy link

Choose a reason for hiding this comment

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

Consider exiting early here with the negative condition to make the precondition clear and the code below more width-efficient.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done.

const meta =
head.querySelector('meta[name="amp-variable-substitution-whitelist"]');

if (meta) {
Copy link

Choose a reason for hiding this comment

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

ditto

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done.

@@ -84,6 +84,39 @@ describe('VariableSource', () => {
});
});

describes.fakeWin('#whitelist', {
Copy link

Choose a reason for hiding this comment

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

I think I'd much prefer using describes.realWin to create a real <meta> tag and use real querySelector. The test would be much useful that way, since it actually tests the real thing that will be running under the code in prod.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done.

@@ -246,6 +261,11 @@ export class VariableSource {
* @private
*/
buildExpr_(keys, isV2) {
// If a whitelist is provided, the keys must all belong to the whitelist.
Copy link

Choose a reason for hiding this comment

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

Could you clarify a bit more about why they keys must all belong to the whitelist?

Copy link
Owner Author

Choose a reason for hiding this comment

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

Do you want me to explain this in the comments? This is basically because additional keys can be provided to the getExpr function (which then call the build_expr_). We already made sure that we filter those keys that are provided in the set and setAsync. So it is necessary to have this final filtering as well. And I insist that we do it in here rather than in getExpr. This was we make sure that nothing slips through. Does that make sense?

Copy link

@zhangsu zhangsu Feb 15, 2018

Choose a reason for hiding this comment

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

I feel like this this not very clear, so definitely worth clarifying this in the code instead of on Github. :)

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done.


if (meta) {
return meta.getAttribute('content').split(',')
.map(variable => variable.trim());
Copy link

Choose a reason for hiding this comment

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

Just to make sure: do we plan to move this to a utility after the two pull requests are merged (consider filing a bug so that you don't forget)?

Copy link
Owner Author

Choose a reason for hiding this comment

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

Yes. That is the plan. I filed the bug ampproject#13497.

@hamousavi
Copy link
Owner Author

@zhangsu my linter is giving me some error

Error: Failed to load plugin sort-imports-es6-autofix: Cannot find module 'eslint-plugin-sort-imports-es6-autofix'

have you experienced something like this? It has been working with my previous PR.

Copy link

@zhangsu zhangsu left a comment

Choose a reason for hiding this comment

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

Not sure what's going on with the linter failure, try merging the latest upstream and rerun the linter. I think there's also a way to clear your local Gulp build cache so that you can rebuild everything.


// The whitelist of variables allowed for variable substitution.
/** @const @private {?Array<string>} */
this.whitelist_ = this.getVariableWhitelist_();
Copy link

Choose a reason for hiding this comment

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

Should this be initialized to null to enable lazy initialization, just like your other PR?

Copy link
Owner Author

Choose a reason for hiding this comment

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

That was my first approach but then I changed it because the lazy initialization in this PR will add a large number of unnecessary conditional checks: To lazy initialize we must move the initialization to set and setAsync. Now look at the initialize method of GlobalVariableSource. There are tens of calls to set and setAsync. So if we do lazy loading then runtime has to check a conditional for every one of these calls to set. It gets even worse if one notices that the check for lazy initialization must also appear in getExpr.

Also since the sole purpose of this class is to build a regex for variables that are allowed to be substituted, we can be certain that when the class is instantiated, a call that triggers the lazy initialization of whitelist will happen.

This last point is different in the action PR. You could have a document that contains no actions on the special AMP target, but the standard action handler is instantiated nevertheless (as there could be other types of actions).

These being said, we could just remove all the filterings happening in set as we are going to do one final filtering in buildExpr_ anyway. In that case we can just lazy initialize at buildExpr_ time. Let me know what you think.

Copy link

Choose a reason for hiding this comment

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

I think we should generally avoid premature micro-(and macro-)optimizations unless we have profiling or benchmark data to support the claim for the performance difference. For conditional checks like this, I tend to trust the JIT compiler and the JavaScript virtual machine to perform the right optimization, so I'd aim for maximum code clarity instead of performance. In this case, the code is readable either ways, so I don't really feel strong about either ways.

@@ -121,6 +127,43 @@ export class VariableSource {

/** @private {boolean} */
this.initialized_ = false;

// The whitelist of variables allowed for variable substitution.
Copy link

Choose a reason for hiding this comment

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

Add this to the JSDoc:

/**
 * The whitelist of variables allowed for variable substitution.
 * @const @private {?Array<string>}
 */

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done.

* @param {!./ampdoc-impl.AmpDoc} ampdoc
*/
constructor(ampdoc) {
/** @protected @const {?./ampdoc-impl.AmpDoc} */
Copy link

Choose a reason for hiding this comment

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

Nit: Let's be consistent on whether @const is before visibility tag or not. On line 131 below @const is before the visibility tag @private, but not here.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done.

@@ -109,7 +109,13 @@ export function getNavigationData(win, attribute) {
* and override initialize() to add more supported variables.
*/
export class VariableSource {
constructor() {
/**
* @param {!./ampdoc-impl.AmpDoc} ampdoc
Copy link

Choose a reason for hiding this comment

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

This is marked as non-nullable, but the field below is marked as nullable, which doesn't make sense. The field is marked as @const, which means that it's never re-assigned, so it should always have the same nullability as the constructor parameter here.

Can we fix the test instead?

Copy link
Owner Author

Choose a reason for hiding this comment

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

I fixed the variable-source test, but many of the other tests are failing unfortunately. I added back the check for this.ampdoc in getVariableWhitelist. I could create a bug maybe?

Copy link

Choose a reason for hiding this comment

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

If the code is checking for whether this.ampdoc is truthy, then the type annotation here should really be consistent (i.e., ?AmpDoc. If the type is marked as non-nullable, then the code should do null check; if the code is doing null check, then the type should be marked as nullable.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done.

// If a whitelist is provided, the keys must all belong to the whitelist.
if (this.getVariableWhitelist_()) {
keys = keys.filter(key =>
this.getVariableWhitelist_().includes(key));
Copy link

Choose a reason for hiding this comment

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

Nit: this seems to fit on the previous line.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done.

getAttribute: () => 'ABC,ABCD,CANONICAL',
};
sandbox.stub(env.win.document.head,
'querySelector').callsFake(() => fakeMeta);
Copy link

Choose a reason for hiding this comment

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

This file is using a real window for each test, so it seems to me that we can use real querySelector and insert real meta tag here. This is especially valuable for a functional test as it's supposed to test the integration.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done.

*/
isWhitelisted_(varName) {
return this.getVariableWhitelist_() &&
!this.getVariableWhitelist_().includes(varName);
Copy link

Choose a reason for hiding this comment

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

This checks if the whitelist does not include the variable name, which contradicts with the method name isWhitelisted_.

I think this method should

return !this.getVariableWhitelist_() ||
    this.getVariableWhitelist_().includes(varName);

And the call sites should be checking if (!this.isWhitelisted_(...)).

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done.


/**
* @return {?Array<string>} The whitelist of allowed AMP variables. (if provided in
a meta tag).
Copy link

Choose a reason for hiding this comment

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

Nit: missing * at the start of the JSDoc column.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done.


// A meta[name="amp-variable-substitution-whitelist"] tag, if present,
// contains, in its content attribute, a whitelist of variable
// substitution.
Copy link

Choose a reason for hiding this comment

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

Nit: this seems to fit on the previous line.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done.

/**
* @param {string} varName
* @return {boolean} If a whitelist is provided and
it contains the variable name returns true.
Copy link

Choose a reason for hiding this comment

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

Nit: part of this line seems to fit on the previous line.

Also missing a starting *.

Actually, I think it'd be more naturally to just say:

/**
 * Returns `true` if a variable whitelist is *not* present or the present
 * whitelist contains the given variable name.
 * @param {string} varName
 * @return {boolean}
 */

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done.

@@ -109,7 +109,13 @@ export function getNavigationData(win, attribute) {
* and override initialize() to add more supported variables.
*/
export class VariableSource {
constructor() {
/**
* @param {!./ampdoc-impl.AmpDoc} ampdoc
Copy link

Choose a reason for hiding this comment

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

If the code is checking for whether this.ampdoc is truthy, then the type annotation here should really be consistent (i.e., ?AmpDoc. If the type is marked as non-nullable, then the code should do null check; if the code is doing null check, then the type should be marked as nullable.

* @param {!./ampdoc-impl.AmpDoc} ampdoc
*/
constructor(ampdoc) {
/** @protected @const {!./ampdoc-impl.AmpDoc} */
Copy link

Choose a reason for hiding this comment

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

Can this be @private (and field renamed to ampdoc_)?

Copy link
Owner Author

Choose a reason for hiding this comment

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

No. The derived GlobalVariableSource class uses ampdoc quite a lot.

* The whitelist of variables allowed for variable substitution.
* @private @const {?Array<string>}
*/
this.whitelist_ = this.getVariableWhitelist_();
Copy link

Choose a reason for hiding this comment

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

I still think the field name should be consistent with the method name, so either change the field name to variableWhitelist_ or change the method name to getWhitelist_.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done.

return this.whitelist_;
}

if (!this.ampdoc) {
Copy link

Choose a reason for hiding this comment

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

Add a TODO to remove this after fixing the tests?

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done. I'll also create an issue when I create a pull request to upstream.


// The whitelist of variables allowed for variable substitution.
/** @const @private {?Array<string>} */
this.whitelist_ = this.getVariableWhitelist_();
Copy link

Choose a reason for hiding this comment

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

I think we should generally avoid premature micro-(and macro-)optimizations unless we have profiling or benchmark data to support the claim for the performance difference. For conditional checks like this, I tend to trust the JIT compiler and the JavaScript virtual machine to perform the right optimization, so I'd aim for maximum code clarity instead of performance. In this case, the code is readable either ways, so I don't really feel strong about either ways.

Copy link
Owner Author

@hamousavi hamousavi left a comment

Choose a reason for hiding this comment

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

@zhangsu do you think we are ready to make PR to upstream?

@@ -109,7 +109,13 @@ export function getNavigationData(win, attribute) {
* and override initialize() to add more supported variables.
*/
export class VariableSource {
constructor() {
/**
* @param {!./ampdoc-impl.AmpDoc} ampdoc
Copy link
Owner Author

Choose a reason for hiding this comment

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

Done.

* @param {!./ampdoc-impl.AmpDoc} ampdoc
*/
constructor(ampdoc) {
/** @protected @const {!./ampdoc-impl.AmpDoc} */
Copy link
Owner Author

Choose a reason for hiding this comment

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

No. The derived GlobalVariableSource class uses ampdoc quite a lot.

* The whitelist of variables allowed for variable substitution.
* @private @const {?Array<string>}
*/
this.whitelist_ = this.getVariableWhitelist_();
Copy link
Owner Author

Choose a reason for hiding this comment

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

Done.

return this.whitelist_;
}

if (!this.ampdoc) {
Copy link
Owner Author

Choose a reason for hiding this comment

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

Done. I'll also create an issue when I create a pull request to upstream.

@zhangsu
Copy link

zhangsu commented Feb 16, 2018

Yes, go head!

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

Successfully merging this pull request may close these issues.

2 participants