Repository: oppia/oppia

Issue:
Add Stylelint to lint css files
Currently we are eyeballing the CSS styles during code reviews, it would be nice to implement some sort of linting like we do for JS and Python files. ~A good css linter is [CSSLint](https://github.com/CSSLint/csslint).~ We will be using Stylelint because it can lint CSS within HTML files. 

Note to devs:
- Take a look at `scripts/install_third_party.sh` for how to install Stylelint via node. For linting execution, take a look at `scripts/pre_commit_linter.py` (note that multi-threading is involved). 
- Take a look at https://stylelint.io/#getting-started for guidance on installation and configuration. Please note that because we are using node in the context of the oppia folders, the developer should modify the steps so that it'll be compatible with our installation and running processes. 
- Employ the [HTML processor](https://github.com/ccbikai/stylelint-processor-html) so that it can lint CSS within our HTML files. 
- Target linting files: main [css file](https://github.com/oppia/oppia/blob/develop/core/templates/dev/head/css/oppia.css) as well as all html files within `core/templates/dev/head`

Edit: Update description with instructions as well as changing CSS linter.


Discussion:
Unfortunately, I think it can only lint pure CSS files (do double check), so it'll only be `oppia.css` for now until all the css has been separated.

Note to developer who picks this up: PR #1996 (now closed) will come in handy because a more flexible multi-threading approach will need to be used. 

Actually I think multithreading is orthogonal to the CSS linter; we can do that separately. (Though, do we need to do it at all? Apparently it was due to a bug in jscs...)

It is orthogonal to the CSS linter, but to not crash developer's systems,
it would be best if multi-threading were to be implemented before or with
CSS linter. For less powerful machines, running all three linters at once
might be overpowering (assuming they all were working properly).

On Sun, Jun 19, 2016 at 8:04 PM, Sean Lip notifications@github.com wrote:

> Actually I think multithreading is orthogonal to the CSS linter; we can do
> that separately. (Though, do we need to do it at all? Apparently it was due
> to a bug in jscs...)
> 
> —
> You are receiving this because you authored the thread.
> Reply to this email directly, view it on GitHub
> https://github.com/oppia/oppia/issues/1977#issuecomment-227041424, or mute
> the thread
> https://github.com/notifications/unsubscribe/AFfdMA_3lMBMC4dRCiCEa8ECRa2fNyZfks5qNgM0gaJpZM4IuFPn
> .

@TruongKim, is this something you might be interested in looking into? 

@seanlip, I found this other CSS linting tool --- [Stylelint](https://stylelint.io/) and apparently it can lint CSS within `<style>` tags with a processor like https://github.com/ccbikai/stylelint-processor-html. Do you think it might be better with this instead of CSSLint? thanks! 

Hm, for this sort of thing, I think the proof is in the pudding. As long as a tool like this works and does what we need it to, it should be fine!
sgtm!

> On Feb 13, 2017, at 3:16 PM, Sean Lip <notifications@github.com> wrote:
> 
> Hm, for this sort of thing, I think the proof is in the pudding. As long as a tool like this works and does what we need it to, chances are that it's probably fine.
> 
> —
> You are receiving this because you were assigned.
> Reply to this email directly, view it on GitHub, or mute the thread.
> 

Hello @seanlip and @kevinlee12!  I am planning to work on this issue. Since, by default all rules in `stylelint` are turned off, can you please list or point out the rules we need to apply (or do we have a design doc for this too)? I tried using both - the `stylelint-config-standard` and the `stylelint-config-recommended` but both of them list a number of errors with `oppia.css`.  
Thanks!
Hi @apb7 -- we don't have a design doc for this. Could you perhaps create one? I suggest that you propose a set of rules that you think are suitable, and highlight the discrepancies between that set of rules and oppia.css (as comments in the doc). We can then go through these and resolve them.

Thanks!
That sounds good, @seanlip!  
Thanks! 
Hello @seanlip!  
I've created a design doc for this issue: https://docs.google.com/document/d/1QfiIQIu3JveyOJan3c4soRvjy0qoFPzkz4hQ8OgLvQ4/edit?usp=sharing  
Thanks!
 
Hey @seanlip! I've updated the design doc. 

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
@@ -300,6 +300,55 @@ def _get_all_files_in_directory(dir_path, excluded_glob_patterns):
     return files_in_directory
 
 
+def _lint_css_files(stylelint_path, config_path, files_to_lint, stdout, result):
+    """Prints a list of lint errors in the given list of CSS files.
+
+    Args:
+        stylelint_path: str. Path to the Stylelint binary.
+        config_path: str. Path to the configuration file.
+        files_to_lint: list(str). A list of filepaths to lint.
+        stdout:  multiprocessing.Queue. A queue to store Stylelint outputs.
+        result: multiprocessing.Queue. A queue to put results of test.
+    """
+    start_time = time.time()
+    num_files_with_errors = 0
+
+    num_css_files = len(files_to_lint)
+    if not files_to_lint:
+        result.put('')
+        print 'There are no CSS files to lint.'
+        return
+
+    print 'Total css files: ', num_css_files
+    stylelint_cmd_args = [
+        stylelint_path, '--config=' + config_path]
+    for _, filename in enumerate(files_to_lint):
+        print 'Linting: ', filename
+        proc_args = stylelint_cmd_args + [filename]
+        proc = subprocess.Popen(
+            proc_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+
+        linter_stdout, linter_stderr = proc.communicate()
+        if linter_stderr:
+            print 'LINTER FAILED'
+            print linter_stderr
+            sys.exit(1)
+
+        if linter_stdout:
+            num_files_with_errors += 1
+            print linter_stdout
+            stdout.put(linter_stdout)
+
+    if num_files_with_errors:
+        result.put('%s    %s CSS file' % (
+            _MESSAGE_TYPE_FAILED, num_files_with_errors))
+    else:
+        result.put('%s   %s CSS file linted (%.1f secs)' % (
+            _MESSAGE_TYPE_SUCCESS, num_css_files, time.time() - start_time))
+
+    print 'CSS linting finished.'
+
+
 def _lint_js_files(
         node_path, eslint_path, files_to_lint, stdout, result):
     """Prints a list of lint errors in the given list of JavaScript files.
@@ -310,9 +359,6 @@ def _lint_js_files(
         files_to_lint: list(str). A list of filepaths to lint.
         stdout:  multiprocessing.Queue. A queue to store ESLint outputs.
         result: multiprocessing.Queue. A queue to put results of test.
-
-    Returns:
-        None
     """
     start_time = time.time()
     num_files_with_errors = 0
@@ -359,9 +405,6 @@ def _lint_py_files(config_pylint, config_pycodestyle, files_to_lint, result):
         config_pycodestyle: str. Path to the tox.ini file.
         files_to_lint: list(str). A list of filepaths to lint.
         result: multiprocessing.Queue. A queue to put results of test.
-
-    Returns:
-        None
     """
     start_time = time.time()
     are_there_errors = False
@@ -526,39 +569,74 @@ def _pre_commit_linter(all_files):
         parent_dir, 'oppia_tools', 'node-6.9.1', 'bin', 'node')
     eslint_path = os.path.join(
         parent_dir, 'node_modules', 'eslint', 'bin', 'eslint.js')
-
-    if not os.path.exists(eslint_path):
+    stylelint_path = os.path.join(
+        parent_dir, 'node_modules', 'stylelint', 'bin', 'stylelint.js')
+    config_path_for_css_in_html = os.path.join(
+        parent_dir, 'oppia', '.stylelintrc')
+    config_path_for_oppia_css = os.path.join(
+        parent_dir, 'oppia', 'core', 'templates', 'dev', 'head',
+        'css', '.stylelintrc')
+    if not (os.path.exists(eslint_path) and os.path.exists(stylelint_path)):
         print ''
         print 'ERROR    Please run start.sh first to install node-eslint '
-        print '         and its dependencies.'
+        print '         or node-stylelint and its dependencies.'
         sys.exit(1)
 
     js_files_to_lint = [
         filename for filename in all_files if filename.endswith('.js')]
     py_files_to_lint = [
         filename for filename in all_files if filename.endswith('.py')]
+    html_files_to_lint_for_css = [
+        filename for filename in all_files if filename.endswith('.html')]
+    css_files_to_lint = [
+        filename for filename in all_files if filename.endswith('oppia.css')]
+
+    css_in_html_result = multiprocessing.Queue()
+    css_in_html_stdout = multiprocessing.Queue()
 
-    js_result = multiprocessing.Queue()
     linting_processes = []
+    linting_processes.append(multiprocessing.Process(
+        target=_lint_css_files, args=(
+            stylelint_path,
+            config_path_for_css_in_html,
+            html_files_to_lint_for_css, css_in_html_stdout,
+            css_in_html_result)))
+
+    css_result = multiprocessing.Queue()
+    css_stdout = multiprocessing.Queue()
+
+    linting_processes.append(multiprocessing.Process(
+        target=_lint_css_files, args=(
+            stylelint_path,
+            config_path_for_oppia_css,
+            css_files_to_lint, css_stdout,
+            css_result)))
+
+    js_result = multiprocessing.Queue()
     js_stdout = multiprocessing.Queue()
+
     linting_processes.append(multiprocessing.Process(
         target=_lint_js_files, args=(
             node_path, eslint_path, js_files_to_lint,
             js_stdout, js_result)))
 
     py_result = multiprocessing.Queue()
+
     linting_processes.append(multiprocessing.Process(
         target=_lint_py_files,
         args=(config_pylint, config_pycodestyle, py_files_to_lint, py_result)))
-    print 'Starting Javascript and Python Linting'
+
+    print 'Starting CSS, Javascript and Python Linting'
     print '----------------------------------------'
+
     for process in linting_processes:
+        process.daemon = True
         process.start()
 
     for process in linting_processes:
         # Require timeout parameter to prevent against endless waiting for the
         # linting function to return.
-        process.join(timeout=600)
+        process.join(timeout=200)
 
     js_messages = []
     while not js_stdout.empty():
@@ -568,6 +646,8 @@ def _pre_commit_linter(all_files):
     print '\n'.join(js_messages)
     print '----------------------------------------'
     summary_messages = []
+    summary_messages.append(css_in_html_result.get())
+    summary_messages.append(css_result.get())
     summary_messages.append(js_result.get())
     summary_messages.append(py_result.get())
     print '\n'.join(summary_messages)


Test patch: