1

In my compressed code, under advanced compilation, the compiler has changed the calling context of my function. I'm after some reasoning why and how, so I can figure out how to fix it.

Back story

I've generated my code into modules and for the past few days I've been converting the react js material ui library into a closure style provide/require syntax. I couldn't get CommonJS to play nicely with my modular approach and I couldn't get goog.module to work with the debug tool I use 'plovr'. Almost there but I'm stumbling with this.

My compiled code has sourcemaps so I can see where it's going wrong and it doesn't seem to make any sense to me.

The error throws here. Note that this is compressed code but you are seeing it mapped to the original code via sourcemaps. decomposeColor doesn't exist because this is equal to the window object. enter image description here

If I type this into the console. enter image description here

I then go one level up the stack and type this into the console and it's the correct object I would expect to see one level down. enter image description here enter image description here

Here's the same thing but what the actual code looks like compressed enter image description here enter image description here

Any idea what can cause the compiler to do this?

UPDATE:

After some pointers in the comments (Thanks Jan) it made sense what I should be looking for, it seems the compiler has converted from my object method

goog.provide('mui.utils.colorManipulator');
mui.utils.colorManipulator = {
  //...
  /**
   * @this {mui.utils.colorManipulator}
   */
  fade: function fade(color, amount) {
    color = this._decomposeColor(color);
    if (color.type === 'rgb' || color.type === 'hsl') color.type += 'a';
    return this._convertColorToString(color, amount);
  }
  //...
}

into a function declared at the global scope.

function kc(f, a) {
  f = this.nd(f);
  if ("rgb" === f.type || "hsl" === f.type)
    f.type += "a";
  return this.md(f, a)
}

So the 'this' contexts will be different, I just need to figure out why the compiler would do that.

Update:

Here's all the code for the colorManipulator. It's pretty much ported from this this

goog.provide('mui.utils.colorManipulator')

mui.utils.colorManipulator = {

  /**
   * The relative brightness of any point in a colorspace, normalized to 0 for
   * darkest black and 1 for lightest white. RGB colors only. Does not take
   * into account alpha values.
   *
   * TODO:
   * - Take into account alpha values.
   * - Identify why there are minor discrepancies for some use cases
   *   (i.e. #F0F & #FFF). Note that these cases rarely occur.
   *
   * Formula: http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
   */
  _luminance(color) {
    color = this._decomposeColor(color);

    if (color.type.indexOf('rgb') > -1) {
      let rgb = color.values.map((val) => {
        val /= 255; // normalized
        return val <= 0.03928 ? val / 12.92 : Math.pow((val + 0.055) / 1.055, 2.4);
      });

      return 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2];

    }
    else {
      let message = 'Calculating the relative luminance is not available for ' +
                    'HSL and HSLA.';
      console.error(message);
      return -1;
    }
  },

  /**
   * @params:
   * additionalValue = An extra value that has been calculated but not included
   *                   with the original color object, such as an alpha value.
   */
  _convertColorToString(color, additonalValue) {
    let str = color.type + '(' +
              parseInt(color.values[0]) + ',' +
              parseInt(color.values[1]) + ',' +
              parseInt(color.values[2]);

    if (additonalValue !== undefined) {
      str += ',' + additonalValue + ')';
    }
    else if (color.values.length === 4) {
      str += ',' + color.values[3] + ')';
    }
    else {
      str += ')';
    }

    return str;
  },

  // Converts a color from hex format to rgb format.
  _convertHexToRGB(color) {
    if (color.length === 4) {
      let extendedColor = '#';
      for (let i = 1; i < color.length; i++) {
        extendedColor += color.charAt(i) + color.charAt(i);
      }
      color = extendedColor;
    }

    let values = {
      r:    parseInt(color.substr(1,2), 16),
      g:    parseInt(color.substr(3,2), 16),
      b:    parseInt(color.substr(5,2), 16),
    };

    return 'rgb(' + values.r + ',' +
                    values.g + ',' +
                    values.b + ')';
  },

  // Returns the type and values of a color of any given type.
  _decomposeColor(color) {
    if (color.charAt(0) === '#') {
      return this._decomposeColor(this._convertHexToRGB(color));
    }

    let marker = color.indexOf('(');
    let type = color.substring(0, marker);
    let values = color.substring(marker + 1, color.length - 1).split(',');

    return {type: type, values: values};
  },

  // Set the absolute transparency of a color.
  // Any existing alpha values are overwritten.
  /**
   * @this {mui.utils.colorManipulator}
   */
  fade(color, amount) {
    color = this._decomposeColor(color);
    if (color.type === 'rgb' || color.type === 'hsl') color.type += 'a';
    return this._convertColorToString(color, amount);
  },

  // Desaturates rgb and sets opacity to 0.15
  lighten(color, amount) {
    color = this._decomposeColor(color);

    if (color.type.indexOf('hsl') > -1) {
      color.values[2] += amount;
      return this._decomposeColor(this._convertColorToString(color));
    }
    else if (color.type.indexOf('rgb') > -1) {
      for (let i = 0; i < 3; i++) {
        color.values[i] *= 1 + amount;
        if (color.values[i] > 255) color.values[i] = 255;
      }
    }

    if (color.type.indexOf('a') <= -1) color.type += 'a';

    return this._convertColorToString(color, '0.15');
  },

  darken(color, amount) {
    color = this._decomposeColor(color);

    if (color.type.indexOf('hsl') > -1) {
      color.values[2] += amount;
      return this._decomposeColor(this._convertColorToString(color));
    }
    else if (color.type.indexOf('rgb') > -1) {
      for (let i = 0; i < 3; i++) {
        color.values[i] *= 1 - amount;
        if (color.values[i] < 0) color.values[i] = 0;
      }
    }

    return this._convertColorToString(color);
  },


  // Calculates the contrast ratio between two colors.
  //
  // Formula: http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
  contrastRatio(background, foreground) {
    let lumA = this._luminance(background);
    let lumB = this._luminance(foreground);

    if (lumA >= lumB) {
      return ((lumA + 0.05) / (lumB + 0.05)).toFixed(2);
    }
    else {
      return ((lumB + 0.05) / (lumA + 0.05)).toFixed(2);
    }
  },

  /**
   * Determines how readable a color combination is based on its level.
   * Levels are defined from @LeaVerou:
   * https://github.com/LeaVerou/contrast-ratio/blob/gh-pages/contrast-ratio.js
   */
  contrastRatioLevel(background, foreground) {
    let levels = {
      'fail': {
        range: [0, 3],
        color: 'hsl(0, 100%, 40%)',
      },
      'aa-large': {
        range: [3, 4.5],
        color: 'hsl(40, 100%, 45%)',
      },
      'aa': {
        range: [4.5, 7],
        color: 'hsl(80, 60%, 45%)',
      },
      'aaa': {
        range: [7, 22],
        color: 'hsl(95, 60%, 41%)',
      },
    };

    let ratio = this.contrastRatio(background, foreground);

    for (let level in levels) {
      let range = levels[level].range;
      if (ratio >= range[0] && ratio <= range[1]) return level;
    }
  },
};
Ally
  • 4,894
  • 8
  • 37
  • 45
  • Please paste the relevant code into your question in TEXT format so we can see the relevant code. Screen shots are extremely hard to read and impossible to copy/paste from for answers. Plus ALL questions about `this` must include the context of the code that calls the function because that is what determines how `this` is set. – jfriend00 Aug 21 '15 at 22:50
  • ...that you're actively telling the compiler to restructure your code aggressively using `advanced optimization`? Have you checked the documentation of this feature? https://developers.google.com/closure/compiler/docs/api-tutorial3?hl=en – Jan Aug 21 '15 at 22:52
  • @jfriend00 If I pasted the code, there's so much of it, I'm not sure that's the best thing to do. And I can't really format it down into a smaller use case. Happy to post any specific bits up you think might be relevant, you can get an idea of the functions by looking at that call stack on the right. – Ally Aug 21 '15 at 22:59
  • @Jan Yeah I use the feature a fair bit and have studied as much as I can glean from the docs. One point to note is I have the `use_types_for_optimization` set to `true` which I suspect might have something to do with it – Ally Aug 21 '15 at 23:01
  • 1
    Your responsibility (if you want the best chances of getting help here) is to reduce the problem down to its simplest form and post the code for that simpler form. Quoted from [How do I ask a good question](http://stackoverflow.com/help/how-to-ask) in the StackOverflow help center is this **"Include just enough code to allow others to reproduce the problem"**. – jfriend00 Aug 21 '15 at 23:04
  • 1
    Well, all I can tell you is that it looks like `fade` in your code is a method on an object, and in the compressed version it's not. As if, for example, it was a function created on another function which in your code is instantiated by `new` but in the compressed version it's not. Or maybe it was created using `this.fade =...` in a context where `this` meant the global object. Without seeing the object/function/method, like @jfriend00 said, that's all I can tell you. – Jan Aug 21 '15 at 23:06
  • 1
    Did you look at the actual optimized code to see exactly what the optimization did? It could have inlined your method so there is no function any more and thus no `this` set at all. – jfriend00 Aug 21 '15 at 23:08
  • @Jan Cheers Jan that was exactly the pointer I needed. You were right, I've updated my question to match, just need to work out why the compiler would do that. But I could probably alter the code structure to work around it. – Ally Aug 21 '15 at 23:33
  • As to the "why" I once again refer you to the docs. The compiler probably thought the object was unnecessary because it can't find a reference to it inside YOUR code. They had a few cases and suggestions in the docs, about exporting symbols or compiling ALL of your libraries together etc. – Jan Aug 21 '15 at 23:36
  • @jfriend00 I can't minimise into the smallest example because then the compiler will compress differently and the problem may disappear – Ally Aug 21 '15 at 23:47
  • Somewhat by definition, the smallest example that demonstrates the problem does NOT make the problem disappear. FWIW, the code you added to your question vastly improves your question. That's the kind of thing the help center is talking about doing. I think you should probably add to that the calling context in both unoptimized and optimized versions for people to really see the context here. – jfriend00 Aug 21 '15 at 23:54
  • @Jan The documentation you linked to me (for the second time) is not relevant. Exports and externs aren't what's needed here as all the code is self contained – Ally Aug 22 '15 at 00:01
  • Would instantiating the mui.utils.colorManipulator through a constructor minify the same way? `mui.utils.colorManipulator = new function() { this.fade = function... };` – Jan Aug 22 '15 at 00:04
  • @Jan Yeah that should do it, that's likely how I think I'm going to solve it. – Ally Aug 22 '15 at 00:08
  • @Deryck Thanks Deryck, helpful. – Ally Aug 22 '15 at 00:09
  • FYI, since from the little I can see `mui.utils.colorManipulator` is either a singleton or a global, you could just replace references inside the methods of that object to `this` with `mui.utils.colorManipulator` and that would still allow the compiler to optimize the fact that this is a singleton. – jfriend00 Aug 22 '15 at 00:13
  • @jfriend00 The entire `mui.utils.colorManipulator` object has been removed though. Maybe referencing it from inside the method would make it NOT be removed (and solve the issue). Or point the minified references correctly. But, just saying... And even if it works, it's not so nice to code that way with a bunch of hard references. Might be interesting to know if that solves the issue though. Sounds to me like one of those issues/libraries where a fix today might not work next week though. – Jan Aug 22 '15 at 00:15
  • @Jan - If it's a singleton, then all it really is a namespace for some globally accessible functions. By all rights, it can be removed. The desired `this` value is constant since there's only ever one instance of the object in the first place and all I see are methods, not instance data. The compiler should love that situation and should do well. That's the theory anyway. I don't have your code or any way to test it. If a global object reference doesn't work, then the whole compiler is busted - time to throw it away. Isssues with `this` are much more subtle. – jfriend00 Aug 22 '15 at 00:18
  • @Jan that's the common way to do things with the closure-library e.g. you might use something like `goog.array.find` which is really the same thing as doing `mui.utils.colorManipulator._decomposeColor`. It annoyingly causes really long lines of code. [goog.module](https://docs.google.com/document/d/1SwVn2ajodVsAauhJoHOyQgr87bMzaK13ke2CpGorbUY/edit) sets out to solving this. I think doing that will actual solve this problem but I'm not sure if you are right that the compile thinks this object isn't used as it is what the `this` context is one function up the stack, although can't be sure. – Ally Aug 22 '15 at 00:26
  • @jfriend00 I think you're right, thanks, that's the quickest way to solve it here, shame I'm going to probably have to do that in hundreds of other places too. – Ally Aug 22 '15 at 00:27
  • `.fade()` is a pure function that does not manipulate anything inside of the object from which it is called. If the other methods have this in common, they would have been moved up as well, making the `this` context irrelevant in this code. – Deryck Aug 22 '15 at 00:29
  • @jfriend00 I don't know if it's correct to call it a singleton since the constructor is still of the generic Object type. My suggestion with the `new function` constructor would be more of a singleton, being of an (undefined) unique type. But I digress! Yeah, I'm just thinking, if the compiler fails with the current setup (failing to recognize `this` as the object), it's possible it will fail on the hard coded reference to itself as well. I mean, it's already being stupid... ;) And yeah, all we see are methods but OP won't paste their whole code so it's impossible to tell... – Jan Aug 22 '15 at 00:29
  • @Jan - It's a statically declared object. There's only one of them and that reference is stored in a namespaced global - that's what I mean by a global or singleton. The compiler is likely recognizing that and trying to simplify that structure. In fact such a structure with no instance data other than once declared methods can be optimized into nothing more than a bunch of plain function calls since `this` is used for nothing more than reaching a particular function. – jfriend00 Aug 22 '15 at 00:34
  • `this` should be used with objects created with `new` or `.bind`/`.apply`/`.call` – Daniel A. White Aug 22 '15 at 00:35
  • @Jan - that's why my comment that initially discussed this said: ***"all I see are methods, not instance data"***. The OP was free to add more info if that was not the case. But, since the compiler is also deciding to optimize out the object itself, it apparently reached the same conclusion, but is making a mistake in that optimization. – jfriend00 Aug 22 '15 at 00:36
  • @DanielA.White Yes that seems to be the assumption that the compiler operates under – Jan Aug 22 '15 at 00:46
  • @Ally i would say don't depend on that bad pattern. – Daniel A. White Aug 22 '15 at 00:47
  • @DanielA.White Any chance you could elaborate? Happy to be told if I've gone about this the wrong way, what would you recommend? – Ally Aug 22 '15 at 00:49

3 Answers3

3

Implications of object property flattening

In Advanced mode the Compiler collapses object properties to prepare for name shortening. For example, the Compiler transforms this:

var foo = {}; foo.bar = function (a) { alert(a) };
foo.bar("hello"); into this:

var foo$bar = function (a) { alert(a) }; foo$bar("hello"); This property flattening allows the later renaming pass to rename more efficiently. The Compiler can replace foo$bar with a single character, for example.

But property flattening also makes the following practice dangerous:

Using this outside of constructors and prototype methods:

Property flattening can change meaning of the keyword this within a function. For example:

var foo = {}; foo.bar = function (a) { this.bad = a; }; // BAD
foo.bar("hello"); becomes:

var foo$bar = function (a) { this.bad = a; }; foo$bar("hello"); Before the transformation, the this within foo.bar refers to foo. After the transformation, this refers to the global this. In cases like this one the Compiler produces this warning:

"WARNING - dangerous use of this in static method foo.bar" To prevent property flattening from breaking your references to this, only use this within constructors and prototype methods. The meaning of this is unambiguous when you call a constructor with the new keyword, or within a function that is a property of a prototype.

Source: https://developers.google.com/closure/compiler/docs/limitations?hl=en#implications-of-object-property-flattening]

In other words, the closure compiler does not support the use of this in object literals because of property flattening.

Therefore, a simple solution is to reference the full namespace of the object whose property you are trying to access.

mui.utils.colorManipulator.name_of_function(args);
Community
  • 1
  • 1
idream1nC0de
  • 1,171
  • 5
  • 13
  • 2
    I guess this sums it up. The closure compiler is choosing to NOT support some legal Javascript syntax in favor of easier optimizations. It could do the extra work to see that the object is a static object and thus can still be flattened and have `this.method()` replaced with the flattened function call. One could argue it's a bug that they've documented and thus call it a "known limitation". Either way, it makes me even more circumspect about using tools like this that can break perfectly legal code. – jfriend00 Aug 22 '15 at 02:05
  • I will add one more thing. If a C++ compiler had a code optimization setting that caused this kind of problem, it would be considered a serious bug in the optimizer. – jfriend00 Aug 22 '15 at 03:22
  • @jfriend00 the `ADVANCED` mode of the compiler is not designed to be safe for all code - far from it. Make sure you read http://closuretools.blogspot.com/2012/09/which-compilation-level-is-right-for-me.html – Chad Killingsworth Aug 25 '15 at 16:46
2

this shouldn't be really used with object literals as they can be considered like static functions off a namespace. this should be used with functions that are invoked with .bind, .call, .apply or an instance created with a constructor and new. Supposedly it is supported, but closure must not fully understand what is going on: How does "this" keyword work within a function?

I would change the code to refer to the other functions off of mui.utils directly.

Community
  • 1
  • 1
Daniel A. White
  • 187,200
  • 47
  • 362
  • 445
  • I think this pretty much sums it up. Does the compiler assume it's a static function and therefor assumes it won't be using any sort of context in it (`this`)? – Ally Aug 22 '15 at 00:59
  • It should be supported https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Working_with_Objects (see "Defining getters and setters"). IMO the compiler really should be supporting this pattern to identify properties/members and I'd call it a bug. – Jan Aug 22 '15 at 01:06
  • I've pretty much just found this exact use case explained [here](https://developers.google.com/closure/compiler/docs/limitations?hl=en#implications-of-object-property-flattening) – Ally Aug 22 '15 at 01:16
  • I disagree with this. There's no issue with using `this` in any `obj.method()` function call. It makes no difference in the language whether the object is statically defined or dynamically created. If the closure compiler is making some assumption related to this, its a bug not an optimization. Besides `this.method()` is way more compact and extensible should you move away from a singleton in the future than `xxx.yyy.zzz.method()`. – jfriend00 Aug 22 '15 at 02:01
  • @jfriend00 There is a huge problem with that style of code - it breaks if you alias the method. `var foo =obj.method; foo();` – Chad Killingsworth Aug 25 '15 at 16:48
  • @ChadKillingsworth - `var foo = obj.method(); foo()` creates a problem with ALL object oriented Javascript because of the way Javascript binds the `this` argument based on how the method is called so I'm not sure what exactly you're trying to say here. – jfriend00 Aug 25 '15 at 20:51
  • @jfriend00 I'm saying exactly what the compiler did - you should avoid using `this` in a static method and instead use the whole namespace referece. The issue does not come up if you use prototype methods or constructors with prototypes. – Chad Killingsworth Aug 26 '15 at 11:12
0

fade in your code is a method on an object, and in the compressed version it's not, hence the reference to this is changed from the method's object to the global object.

An alternative to solve this would be using the new function singleton pattern to instantiate your object.

mui.utils.colorManipulator = new function() { 
    this.fade = function() { /*...*/ }
};

Either that or like jfriend00 suggested, specify the full namespace inside your methods so that the reference isn't lost on the compiler.

Jan
  • 5,688
  • 3
  • 27
  • 44