Repository: oppia/oppia

Issue:
Add presubmit check to ensure imports are sorted
It's frustrating to examine the dependencies of controllers/directives/factories/etc. b/c they aren't always sorted. This forces developers to linearly search for imports (when eye-balling a file), and often mixes the different "types" of imports we make.

I'd like to have a presubmit check that ensures imports are sorted in the following way:

```javascript
oppia.thing('ThingName', [
  '$sortedDollarImports', 'SortedRegularImports',
  'SORTED_CONSTANT_IMPORTS',
  function(
      $sortedDollarImports, SortedRegularImports,
      SORTED_CONSTANT_IMPORTS) {
    // The implementation of `ThingName`.
  }]);
```

Pointer: Use [di-order](https://github.com/Gillespie59/eslint-plugin-angular/blob/HEAD/docs/rules/di-order.md) rule of the angular plugin of eslint.
Add presubmit check to ensure imports are sorted
It's frustrating to examine the dependencies of controllers/directives/factories/etc. b/c they aren't always sorted. This forces developers to linearly search for imports (when eye-balling a file), and often mixes the different "types" of imports we make.

I'd like to have a presubmit check that ensures imports are sorted in the following way:

```javascript
oppia.thing('ThingName', [
  '$sortedDollarImports', 'SortedRegularImports',
  'SORTED_CONSTANT_IMPORTS',
  function(
      $sortedDollarImports, SortedRegularImports,
      SORTED_CONSTANT_IMPORTS) {
    // The implementation of `ThingName`.
  }]);
```

Pointer: Use [di-order](https://github.com/Gillespie59/eslint-plugin-angular/blob/HEAD/docs/rules/di-order.md) rule of the angular plugin of eslint.


Discussion:

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
@@ -1329,17 +1329,26 @@ def _check_html_directive_name(all_files):
     return summary_messages
 
 
-def _validate_and_parse_js_file(filename, content):
-    """This function validates a JavaScript file and returns the parsed contents
-    as a Python dictionary.
+def _validate_and_parse_js_files(all_files):
+    """This function validates JavaScript files and returns the parsed
+    contents as a Python dictionary.
     """
     # Use Pyjsparser to parse a JS file as a Python dictionary.
     parser = pyjsparser.PyJsParser()
-    print 'Validating and parsing %s file ...' % filename
-    return parser.parse(content)
+    # Select JS files which need to be checked.
+    files_to_check = [
+        filename for filename in all_files if filename.endswith('.js') and
+        not any(fnmatch.fnmatch(filename, pattern) for pattern in
+                EXCLUDED_PATHS)]
+    parsed_js_files = dict()
+    for filename in files_to_check:
+        print 'Validating and parsing %s file ...' % filename
+        content = FileCache.read(filename)
+        parsed_js_files[filename] = parser.parse(content)
+    return parsed_js_files
 
 
-def _check_directive_scope(all_files):
+def _check_directive_scope(all_files, parsed_js_files):
     """This function checks that all directives have an explicit
     scope: {} and it should not be scope: true.
     """
@@ -1347,15 +1356,14 @@ def _check_directive_scope(all_files):
     print '----------------------------------------'
     # Select JS files which need to be checked.
     files_to_check = [
-        filename for filename in all_files if not
-        any(fnmatch.fnmatch(filename, pattern) for pattern in EXCLUDED_PATHS)
-        and filename.endswith('.js')]
+        filename for filename in all_files if filename.endswith('.js') and
+        not any(fnmatch.fnmatch(filename, pattern) for pattern in
+                EXCLUDED_PATHS)]
     failed = False
     summary_messages = []
 
     for filename in files_to_check:
-        content = FileCache.read(filename)
-        parsed_dict = _validate_and_parse_js_file(filename, content)
+        parsed_dict = parsed_js_files[filename]
         with _redirect_stdout(_TARGET_STDOUT):
             # Parse the body of the content as nodes.
             parsed_nodes = parsed_dict['body']
@@ -1470,6 +1478,107 @@ def _check_directive_scope(all_files):
     return summary_messages
 
 
+def _check_sorted_dependencies(all_files, parsed_js_files):
+    """This function checks that the dependencies which are
+    imported in the controllers/directives/factories in JS
+    files are in following pattern: dollar imports, regular
+    imports, and constant imports, all in sorted order.
+    """
+    print 'Starting sorted dependencies check'
+    print '----------------------------------------'
+    files_to_check = [
+        filename for filename in all_files if filename.endswith('.js') and
+        not any(fnmatch.fnmatch(filename, pattern) for pattern in
+                EXCLUDED_PATHS)]
+    properties_to_check = ['controller', 'directive', 'factory']
+    failed = False
+    summary_messages = []
+
+    for filename in files_to_check:
+        parsed_dict = parsed_js_files[filename]
+        with _redirect_stdout(_TARGET_STDOUT):
+            parsed_nodes = parsed_dict['body']
+            for parsed_node in parsed_nodes:
+                if parsed_node['type'] != 'ExpressionStatement':
+                    continue
+                expression = parsed_node['expression']
+                if expression['type'] != 'CallExpression':
+                    continue
+                if expression['callee']['type'] != 'MemberExpression':
+                    continue
+                property_name = expression['callee']['property']['name']
+                if property_name not in properties_to_check:
+                    continue
+                arguments = expression['arguments']
+                if arguments[0]['type'] == 'Literal':
+                    property_value = str(arguments[0]['value'])
+                arguments = arguments[1:]
+                for argument in arguments:
+                    if argument['type'] != 'ArrayExpression':
+                        continue
+                    literal_args = []
+                    function_args = []
+                    dollar_imports = []
+                    regular_imports = []
+                    constant_imports = []
+                    elements = argument['elements']
+                    for element in elements:
+                        if element['type'] == 'Literal':
+                            literal_args.append(str(element['value']))
+                        elif element['type'] == 'FunctionExpression':
+                            func_args = element['params']
+                            for func_arg in func_args:
+                                function_args.append(str(func_arg['name']))
+                    for arg in function_args:
+                        if arg.startswith('$'):
+                            dollar_imports.append(arg)
+                        elif re.search('[a-z]', arg):
+                            regular_imports.append(arg)
+                        else:
+                            constant_imports.append(arg)
+                    dollar_imports.sort()
+                    regular_imports.sort()
+                    constant_imports.sort()
+                    sorted_imports = (
+                        dollar_imports + regular_imports + constant_imports)
+                    if sorted_imports != function_args:
+                        failed = True
+                        print (
+                            'Please ensure that in %s in file %s, the '
+                            'injected dependencies should be in the '
+                            'following manner: dollar imports, regular '
+                            'imports and constant imports, all in sorted '
+                            'order.'
+                            % (property_value, filename))
+                    if sorted_imports != literal_args:
+                        failed = True
+                        print (
+       	                    'Please ensure that in %s in file %s, the '
+       	                    'stringfied dependencies should be in the '
+       	                    'following manner: dollar imports, regular '
+       	                    'imports and constant imports, all in sorted '
+       	                    'order.'
+       	                    % (property_value, filename))
+
+    with _redirect_stdout(_TARGET_STDOUT):
+        if failed:
+            summary_message = (
+                '%s  Sorted dependencies check failed' % (
+                    _MESSAGE_TYPE_FAILED))
+        else:
+            summary_message = (
+                '%s  Sorted dependencies check passed' % (
+                    _MESSAGE_TYPE_SUCCESS))
+
+    summary_messages.append(summary_message)
+    print summary_message
+    print ''
+    print '----------------------------------------'
+    print ''
+
+    return summary_messages
+
+
 def _match_line_breaks_in_controller_dependencies(all_files):
     """This function checks whether the line breaks between the dependencies
     listed in the controller of a directive or service exactly match those
@@ -1784,8 +1893,13 @@ def main():
     files.
     """
     all_files = _get_all_files()
+    parsed_js_files = _validate_and_parse_js_files(
+        all_files)
     linter_messages = _pre_commit_linter(all_files)
-    directive_scope_messages = _check_directive_scope(all_files)
+    directive_scope_messages = _check_directive_scope(
+        all_files, parsed_js_files)
+    sorted_dependencies_messages = _check_sorted_dependencies(
+        all_files, parsed_js_files)
     controller_dependency_messages = (
         _match_line_breaks_in_controller_dependencies(all_files))
     html_directive_name_messages = _check_html_directive_name(all_files)
@@ -1801,7 +1915,8 @@ def main():
     copyright_notice_messages = _check_for_copyright_notice(all_files)
     _print_complete_summary_of_errors()
     all_messages = (
-        directive_scope_messages + controller_dependency_messages +
+        directive_scope_messages + sorted_dependencies_messages +
+        controller_dependency_messages +
         html_directive_name_messages + import_order_messages +
         newline_messages + docstring_messages + comment_messages +
         html_tag_and_attribute_messages + html_linter_messages +


Test patch: