Repository: oppia/oppia

Issue:
Refactor js to use strict equality comparison only [=== instead of ==]
In our current codebase, (referring to js files only) I've noticed that there are a lot of instances of using == for equality comparison. Although this is not buggy in itself, but this can give way to a lot of bugs. I'm not aware whether this has been brought up anytime before or not, but I'll still try to be clear.

Using === offers many advantages over == . I'll be quoting a [SO answer](http://stackoverflow.com/a/359509/4135178) (which has a total score of 3869 at the time of writing this, so I do think that it can be considered credible) and also an [MDN article](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness) in support of this. 

> The == operator will compare for equality after doing any necessary type conversions. The === operator will not do the conversion, so if two values are not the same type === will simply return false.

Typically the same, but using === checks for a strict equality, and eliminates the unnecessary type conversion which takes place in case of == 

> === is faster than ==

This holds true in cases whenever a == condition evaluates to false, since then type conversion takes place and thus causes a performance overhead. (E.g- "0" == 0 initially evaluates to false, but then type conversion takes place and finally it evaluates to true). [Note that if the condition is already true without the need of type conversion (E.g- "abc" == "abc"), then there's no difference in performance!]

> Here are some of the interesting cases:
> 
> ``` javascript
> '' == '0'           // false
> 0 == ''             // true
> 0 == '0'            // true
> false == 'false'    // false
> false == '0'        // true
> false == undefined  // false
> false == null       // false
> null == undefined   // true
> ' \t\r\n ' == 0     // true
> ```
> 
> The lack of transitivity is alarming. My advice is to never use the evil twins. Instead, always use === and !==.  All of the comparisons just shown produce false with the === operator.

^The above excerpt is taken from Douglas Crockford's Javascript: The Good Parts.

Even though this change might be not directly fruitful for the end users, but I do believe that such an effort would help us avoid bugs (false positives) and increase the clarity of code.



Discussion:
I would like to use === and !== everywhere. @souravbadami maybe this could be a pattern check?

Thanks, @526avijitgupta, for the suggestion! 

(Removing TODO:Triage, since this has been confirmed now)

Thanks @seanlip !

@seanlip -- Yes, we can add it.


Resolving patch:
diff --git a/scripts/pre_commit_linter.py b/scripts/pre_commit_linter.py
--- a/scripts/pre_commit_linter.py
+++ b/scripts/pre_commit_linter.py
@@ -74,15 +74,34 @@
     action='store')
 
 BAD_PATTERNS = {
-    '__author__': (
-        'Please remove author tags from this file.'),
-    'datetime.datetime.now()': (
-        'Please use datetime.datetime.utcnow() instead of'
-        'datetime.datetime.now().'),
-    '\t': (
-        'Please use spaces instead of tabs.'),
-    '\r': (
-        'Please make sure all files only have LF endings (no CRLF).')
+    '__author__': {
+        'message': 'Please remove author tags from this file.',
+        'excluded_files': ()},
+    'datetime.datetime.now()': {
+        'message': 'Please use datetime.datetime.utcnow() instead of'
+                   'datetime.datetime.now().',
+        'excluded_files': ()},
+    '\t': {
+        'message': 'Please use spaces instead of tabs.',
+        'excluded_files': ()},
+    '\r': {
+        'message': 'Please make sure all files only have LF endings (no CRLF).',
+        'excluded_files': ()}
+}
+
+BAD_PATTERNS_JS = {
+    ' == ': {
+        'message': 'Please replace == with === in this file.',
+        'excluded_files': (
+            'core/templates/dev/head/expressions/parserSpec.js',
+            'core/templates/dev/head/expressions/evaluatorSpec.js',
+            'core/templates/dev/head/expressions/typeParserSpec.js')},
+    ' != ': {
+        'message': 'Please replace != with !== in this file.',
+        'excluded_files': (
+            'core/templates/dev/head/expressions/parserSpec.js',
+            'core/templates/dev/head/expressions/evaluatorSpec.js',
+            'core/templates/dev/head/expressions/typeParserSpec.js')}
 }
 
 EXCLUDED_PATHS = (
@@ -379,17 +398,30 @@ def _check_bad_patterns(all_files):
     all_files = [
         filename for filename in all_files if not
         any(fnmatch.fnmatch(filename, pattern) for pattern in EXCLUDED_PATHS)]
+    all_js_files = [
+        filename for filename in all_files if filename.endswith('.js')]
     failed = False
     for filename in all_files:
         with open(filename) as f:
             content = f.read()
             total_files_checked += 1
             for pattern in BAD_PATTERNS:
-                if pattern in content:
+                if pattern in content and filename not in (
+                        BAD_PATTERNS[pattern]['excluded_files']):
                     failed = True
                     print '%s --> %s' % (
-                        filename, BAD_PATTERNS[pattern])
+                        filename, BAD_PATTERNS[pattern]['message'])
                     total_error_count += 1
+            if filename in all_js_files:
+                for pattern in BAD_PATTERNS_JS:
+                    if filename not in (
+                            BAD_PATTERNS_JS[pattern]['excluded_files']):
+                        if pattern in content:
+                            failed = True
+                            print '%s --> %s' % (
+                                filename,
+                                BAD_PATTERNS_JS[pattern]['message'])
+                            total_error_count += 1
     if failed:
         summary_message = '%s   Pattern checks failed' % _MESSAGE_TYPE_FAILED
         summary_messages.append(summary_message)


Test patch:
diff --git a/core/tests/karma.conf.js b/core/tests/karma.conf.js
--- a/core/tests/karma.conf.js
+++ b/core/tests/karma.conf.js
@@ -1,5 +1,5 @@
 var argv = require('yargs').argv;
-var isMinificationNeeded = (argv.minify == 'True');
+var isMinificationNeeded = (argv.minify === 'True');
 var generatedJs = 'third_party/generated/dev/js/third_party.js';
 if (isMinificationNeeded) {
   generatedJs = 'third_party/generated/prod/js/third_party.min.js';
diff --git a/core/tests/protractor/i18n.js b/core/tests/protractor/i18n.js
--- a/core/tests/protractor/i18n.js
+++ b/core/tests/protractor/i18n.js
@@ -58,7 +58,7 @@ describe('Site language', function() {
     var options = element.all(by.css('.select2-drop-active li div')).filter(
       function(elem) {
         return elem.getText().then(function(text) {
-          return text == 'Español';
+          return text === 'Español';
         });
       });
     options.first().click();
diff --git a/core/tests/protractor_utils/editor.js b/core/tests/protractor_utils/editor.js
--- a/core/tests/protractor_utils/editor.js
+++ b/core/tests/protractor_utils/editor.js
@@ -358,7 +358,7 @@ var disableGadgetVisibilityForState = function(stateName) {
 var expectGadgetListNameToMatch = function(
     gadgetType, gadgetShortDescription, gadgetName) {
   var expectedListName;
-  if (gadgetShortDescription == gadgetName) {
+  if (gadgetShortDescription === gadgetName) {
     expectedListName = gadgetName;
   } else {
     expectedListName = gadgetShortDescription + ' (' + gadgetName + ')';
@@ -1155,9 +1155,9 @@ var expectGraphComparisonOf = function(v1, v2) {
     element(by.css('.protractor-test-history-graph'))
         .all(by.css('.protractor-test-link')).map(function(link) {
       return link.getCssValue('stroke').then(function(linkColor) {
-        if (linkColor == COLOR_ADDED) {
+        if (linkColor === COLOR_ADDED) {
           return 'added';
-        } else if (linkColor == COLOR_DELETED) {
+        } else if (linkColor === COLOR_DELETED) {
           return 'deleted';
         } else {
           return 'other';
@@ -1176,19 +1176,19 @@ var expectGraphComparisonOf = function(v1, v2) {
         }
       }
 
-      if (totalCount != totalLinks) {
+      if (totalCount !== totalLinks) {
         throw Error(
           'In editor.expectGraphComparisonOf(' + v1 + ', ' + v2 + '), ' +
           'expected to find ' + totalLinks + ' links in total, ' +
           'but found ' + totalCount);
       }
-      if (addedCount != addedLinks) {
+      if (addedCount !== addedLinks) {
         throw Error(
           'In editor.expectGraphComparisonOf(' + v1 + ', ' + v2 + '), ' +
           'expected to find ' + addedLinks + ' added links, ' + 'but found ' +
           addedCount);
       }
-      if (deletedCount != deletedLinks) {
+      if (deletedCount !== deletedLinks) {
         throw Error(
           'In editor.expectGraphComparisonOf(' + v1 + ', ' + v2 + '), ' +
           'expected to find ' + deletedLinks + ' deleted links, ' +
diff --git a/core/tests/protractor_utils/forms.js b/core/tests/protractor_utils/forms.js
--- a/core/tests/protractor_utils/forms.js
+++ b/core/tests/protractor_utils/forms.js
@@ -495,7 +495,7 @@ var CodeMirrorChecker = function(elem) {
           .then(function(lineNumber) {
         // Note: the last line in codemirror will have an empty string for line
         // number and for text. This is to skip that line.
-        if (lineNumber == '') {
+        if (lineNumber === '') {
           return lineNumber;
         }
         if (!compareDict.hasOwnProperty(lineNumber)) {
@@ -511,7 +511,7 @@ var CodeMirrorChecker = function(elem) {
       });
     }).then(function(lineNumbers) {
       var largestLineNumber = lineNumbers[lineNumbers.length - 1];
-      if (largestLineNumber != currentLineNumber) {
+      if (largestLineNumber !== currentLineNumber) {
         _compareTextAndHighlightingFromLine(
           largestLineNumber,
           scrollTo + CODEMIRROR_SCROLL_AMOUNT_IN_PIXELS,