# coding: utf-8
#
# Copyright 2014 The Oppia Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS-IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Pre-commit script for Oppia.

This script lints Python and JavaScript code, and prints a
list of lint errors to the terminal. If the directory path is passed,
it will lint all Python and JavaScript files in that directory; otherwise,
it will only lint files that have been touched in this commit.

This script ignores all filepaths contained within .eslintignore.

=====================
CUSTOMIZATION OPTIONS
=====================
1.  To lint only files that have been touched in this commit
        python -m scripts.linters.pre_commit_linter

2.  To lint all files in the folder or to lint just a specific file
        python -m scripts.linters.pre_commit_linter --path filepath

3.  To lint a specific list of files. Separate filepaths by spaces
        python -m scripts.linters.pre_commit_linter
            --files filepath_1 filepath_2 ... filepath_n

4.  To lint files in verbose mode
        python -m scripts.linters.pre_commit_linter --verbose

5. To lint a specific list of file extensions. Separate file
    extensions by spaces
        python -m scripts.linters.pre_commit_linter
            --only-check-file-extensions py js

Note that the root folder MUST be named 'oppia'.
 """

from __future__ import absolute_import  # pylint: disable=import-only-modules
from __future__ import unicode_literals  # pylint: disable=import-only-modules

import argparse
import fnmatch
import multiprocessing
import os
import re
import subprocess
import sys
import threading

import python_utils

# Install third party dependencies before proceeding.
from . import codeowner_linter
from . import css_linter
from . import general_purpose_linter
from . import html_linter
from . import js_ts_linter
from . import linter_utils
from . import other_files_linter
from . import python_linter
from .. import common
from .. import concurrent_task_utils
from .. import install_third_party_libs

_PARSER = argparse.ArgumentParser()
_EXCLUSIVE_GROUP = _PARSER.add_mutually_exclusive_group()
_PARSER.add_argument(
    '--path',
    help='path to the directory with files to be linted',
    action='store')
_EXCLUSIVE_GROUP.add_argument(
    '--files',
    nargs='+',
    help='specific files to be linted. Space separated list',
    action='store')
_EXCLUSIVE_GROUP.add_argument(
    '--verbose',
    help='verbose mode. All details will be printed.',
    action='store_true')
_PARSER.add_argument(
    '--only-check-file-extensions',
    nargs='+',
    choices=['html', 'css', 'js', 'ts', 'py', 'other'],
    help='specific file extensions to be linted. Space separated list. '
    'If either of js or ts used then both js and ts files will be linted.',
    action='store')

_PARENT_DIR = os.path.abspath(os.path.join(os.getcwd(), os.pardir))

_PATHS_TO_INSERT = [
    os.getcwd(),
    os.path.join(
        common.GOOGLE_APP_ENGINE_SDK_HOME, 'lib', 'yaml-3.10'),
    os.path.join(
        common.GOOGLE_APP_ENGINE_SDK_HOME, 'lib', 'jinja2-2.6'),
    os.path.join(
        common.GOOGLE_APP_ENGINE_SDK_HOME),
    os.path.join(
        _PARENT_DIR, 'oppia_tools', 'webtest-%s' % common.WEBTEST_VERSION),
    os.path.join(
        _PARENT_DIR, 'oppia_tools', 'PyGithub-%s' % common.PYGITHUB_VERSION),
    os.path.join(
        _PARENT_DIR, 'oppia_tools',
        'setuptools-%s' % common.SETUPTOOLS_VERSION),
    os.path.join(
        _PARENT_DIR, 'oppia_tools', 'Pillow-%s' % common.PILLOW_VERSION),
    os.path.join(
        _PARENT_DIR, 'oppia_tools', 'psutil-%s' % common.PSUTIL_VERSION),
    os.path.join(
        _PARENT_DIR, 'oppia_tools', 'pip-tools-%s' % common.PIP_TOOLS_VERSION),
    os.path.join(
        _PARENT_DIR, 'oppia_tools',
        'simple-crypt-%s' % common.SIMPLE_CRYPT_VERSION),
    common.THIRD_PARTY_PYTHON_LIBS_DIR
]

for path in _PATHS_TO_INSERT:
    sys.path.insert(0, path)

_TARGET_STDOUT = python_utils.string_io()
_STDOUT_LIST = multiprocessing.Manager().list()
_FILES = multiprocessing.Manager().dict()


class FileCache(python_utils.OBJECT):
    """Provides thread-safe access to cached file content."""

    def __init__(self):
        self._CACHE_DATA_DICT = {}

    def read(self, filepath, mode='r'):
        """Returns the data read from the file in unicode form.

        Args:
            filepath: str. The file path from which data is to be read.
            mode: str. The mode in which the file is to be opened.

        Returns:
            str. The data read from the file.
        """
        return self._get_data(filepath, mode)[0]

    def readlines(self, filepath, mode='r'):
        """Returns the tuple containing data line by line as read from the
        file in unicode form.

        Args:
            filepath: str. The file path from which data is to be read.
            mode: str. The mode in which the file is to be opened.

        Returns:
            tuple(str). The tuple containing data line by line as read from the
            file.
        """
        return self._get_data(filepath, mode)[1]

    def _get_data(self, filepath, mode):
        """Returns the collected data from the file corresponding to the given
        filepath.

        Args:
            filepath: str. The file path from which data is to be read.
            mode: str. The mode in which the file is to be opened.

        Returns:
            tuple(str, tuple(str)). The tuple containing data read from the file
            as first element and tuple containing the text line by line as
            second element.
        """
        key = (filepath, mode)
        if key not in self._CACHE_DATA_DICT:
            with python_utils.open_file(filepath, mode, newline='') as f:
                lines = f.readlines()
                self._CACHE_DATA_DICT[key] = (''.join(lines), tuple(lines))
        return self._CACHE_DATA_DICT[key]


def _get_linters_for_file_extension(file_extension_to_lint):
    """Return linters for the file extension type.

    Args:
        file_extension_to_lint: str. The file extension to be linted.

    Returns:
        (CustomLintChecks, ThirdPartyLintChecks). A 2-tuple containing objects
        of lint check classes to run in parallel processing.
    """
    parent_dir = os.path.abspath(os.path.join(os.getcwd(), os.pardir))
    custom_linters = []
    third_party_linters = []

    file_extension_type_js_ts = file_extension_to_lint == 'js' or (
        file_extension_to_lint == 'ts')

    if file_extension_type_js_ts:
        general_files_to_lint = _FILES['.js'] + _FILES['.ts']
    elif file_extension_to_lint == 'other':
        general_files_to_lint = _FILES['other']
    else:
        general_files_to_lint = _FILES['.%s' % file_extension_to_lint]

    custom_linter, third_party_linter = general_purpose_linter.get_linters(
        general_files_to_lint, FILE_CACHE)
    custom_linters.append(custom_linter)

    if file_extension_type_js_ts:
        custom_linter, third_party_linter = js_ts_linter.get_linters(
            _FILES['.js'], _FILES['.ts'], FILE_CACHE)
        custom_linters.append(custom_linter)
        third_party_linters.append(third_party_linter)

    elif file_extension_to_lint == 'html':
        custom_linter, third_party_linter = html_linter.get_linters(
            _FILES['.html'], FILE_CACHE)
        custom_linters.append(custom_linter)
        third_party_linters.append(third_party_linter)

        config_path_for_css_in_html = os.path.join(
            parent_dir, 'oppia', '.stylelintrc')
        custom_linter, third_party_linter = css_linter.get_linters(
            config_path_for_css_in_html, _FILES['.html'])
        third_party_linters.append(third_party_linter)

    elif file_extension_to_lint == 'css':
        config_path_for_oppia_css = os.path.join(
            parent_dir, 'oppia', 'core', 'templates', 'css', '.stylelintrc')
        custom_linter, third_party_linter = css_linter.get_linters(
            config_path_for_oppia_css, _FILES['.css'])
        third_party_linters.append(third_party_linter)

    elif file_extension_to_lint == 'py':
        custom_linter, third_party_linter = python_linter.get_linters(
            _FILES['.py'], FILE_CACHE)
        custom_linters.append(custom_linter)
        third_party_linters.append(third_party_linter)

    elif file_extension_to_lint == 'other':
        custom_linter, _ = codeowner_linter.get_linters(FILE_CACHE)
        custom_linters.append(custom_linter)

        custom_linter, _ = other_files_linter.get_linters(FILE_CACHE)
        custom_linters.append(custom_linter)

    return custom_linters, third_party_linters


def _get_changed_filepaths():
    """Returns a list of modified files (both staged and unstaged)

    Returns:
        list. A list of filepaths of modified files.
    """
    unstaged_files = subprocess.check_output([
        'git', 'diff', '--name-only',
        '--diff-filter=ACM']).splitlines()
    staged_files = subprocess.check_output([
        'git', 'diff', '--cached', '--name-only',
        '--diff-filter=ACM']).splitlines()
    all_changed_filepaths = unstaged_files + staged_files
    return [filepath for filepath in all_changed_filepaths]


def _get_all_files_in_directory(dir_path, excluded_glob_patterns):
    """Recursively collects all files in directory and
    subdirectories of specified path.

    Args:
        dir_path: str. Path to the folder to be linted.
        excluded_glob_patterns: set(str). Set of all glob patterns
            to be excluded.

    Returns:
        list. A list of files in directory and subdirectories without excluded
        files.
    """
    files_in_directory = []
    for _dir, _, files in os.walk(dir_path):
        for file_name in files:
            filepath = os.path.relpath(
                os.path.join(_dir, file_name), os.getcwd())
            if not any([
                    fnmatch.fnmatch(filepath, gp) for gp in
                    excluded_glob_patterns]):
                files_in_directory.append(filepath)
    return files_in_directory


def _get_file_extensions(file_extensions_to_lint):
    """This function is used to return the file extensions which need to be
    linted and checked.

    Args:
        file_extensions_to_lint: list(str). The list of file extensions to be
            linted and checked.

    Returns:
        list(str). The list of all file extensions
        to be linted and checked.
    """
    all_file_extensions_type = ['js', 'py', 'html', 'css', 'other']

    if file_extensions_to_lint:
        # Check if 'js' and 'ts' both are present in file_extensions_to_lint.
        js_and_ts_is_present = 'js' in file_extensions_to_lint and (
            'ts' in file_extensions_to_lint)

        if js_and_ts_is_present:
            python_utils.PRINT(
                'Please use only one of "js" or "ts", as we do not have '
                'separate linters for JS and TS files. If both these options '
                'are used together, then the JS/TS linter will be run twice.')
            python_utils.PRINT('Exiting...')
            sys.exit(1)

        return set(file_extensions_to_lint)

    return all_file_extensions_type


def _get_all_filepaths(input_path, input_filenames):
    """This function is used to return the filepaths which needs to be linted
    and checked.

    Args:
        input_path: str. The path of the directory to be linted and checked.
        input_filenames: list(str). The list of filenames to be linted and
            checked, ignored if input_path is specified.

    Returns:
        list(str). The list of filepaths to be linted and checked.
    """
    eslintignore_path = os.path.join(os.getcwd(), '.eslintignore')
    if input_path:
        input_path = os.path.join(os.getcwd(), input_path)
        if not os.path.exists(input_path):
            python_utils.PRINT(
                'Could not locate file or directory %s. Exiting.' % input_path)
            python_utils.PRINT('----------------------------------------')
            sys.exit(1)
        if os.path.isfile(input_path):
            all_filepaths = [input_path]
        else:
            excluded_glob_patterns = FILE_CACHE.readlines(eslintignore_path)
            all_filepaths = _get_all_files_in_directory(
                input_path, excluded_glob_patterns)
    elif input_filenames:
        valid_filepaths = []
        invalid_filepaths = []
        for filename in input_filenames:
            if os.path.isfile(filename):
                valid_filepaths.append(filename)
            else:
                invalid_filepaths.append(filename)
        if invalid_filepaths:
            python_utils.PRINT(
                'The following file(s) do not exist: %s\n'
                'Exiting.' % invalid_filepaths)
            sys.exit(1)
        all_filepaths = valid_filepaths
    else:
        all_filepaths = _get_changed_filepaths()
    all_filepaths = [
        filename for filename in all_filepaths if not
        any(fnmatch.fnmatch(filename, pattern) for pattern in(
            general_purpose_linter.EXCLUDED_PATHS))]
    return all_filepaths


def read_files(file_paths):
    """Read all files to be checked and cache them. This will spin off multiple
    threads to increase the efficiency.
    """
    threads = []
    for file_path in file_paths:
        thread = threading.Thread(target=FILE_CACHE.read, args=(file_path,))
        thread.start()
        threads.append(thread)

    for thread in threads:
        thread.join()


def categorize_files(file_paths):
    """Categorize all the files and store them in shared variable _FILES."""
    all_filepaths_dict = {
        '.py': [], '.html': [], '.ts': [], '.js': [], 'other': [], '.css': []
    }
    for file_path in file_paths:
        _, extension = os.path.splitext(file_path)
        if extension in all_filepaths_dict:
            all_filepaths_dict[extension].append(file_path)
        else:
            all_filepaths_dict['other'].append(file_path)
    _FILES.update(all_filepaths_dict)


def _print_summary_of_error_messages(lint_messages):
    """Print summary of linter error messages.

    Args:
        lint_messages: list(str). List of linter error messages.
    """
    if lint_messages != '':
        error_message_lines = [
            '----------------------------------------',
            'Please fix the errors below:',
            '----------------------------------------',
            ] + lint_messages
        linter_utils.print_failure_message('\n'.join(error_message_lines))


def _get_task_output(lint_messages, failed, task):
    """Returns output of running tasks.

    Args:
        lint_messages: list(str). List of summary messages of linter output.
        failed: bool. The boolean to check if lint checks fail or not.
        task: object(TestingTaskSpec). The task object to get output of linter.

    Returns:
        bool. The boolean to check if the lint checks fail or not.
    """
    if task.task_results:
        for task_result in task.task_results:
            lint_messages += task_result.trimmed_messages
            if task_result.failed:
                failed = True
    return failed


def _print_errors_stacktrace(errors_stacktrace):
    """Print errors stacktrace caught during linter execution.

    Args:
        errors_stacktrace: list(str). List of error stacktrace of lint
            execution failure.
    """
    python_utils.PRINT('')
    python_utils.PRINT(
        'Unable to run the complete lint test, please check '
        'the following stack trace and fix the errors:')
    python_utils.PRINT('+--------------------------+')
    for stacktrace in errors_stacktrace:
        python_utils.PRINT(stacktrace)
        python_utils.PRINT('--------------------------------------------------')
        python_utils.PRINT('')
    python_utils.PRINT('--------------------------------------------------')
    python_utils.PRINT(
        'Some of the linting functions may not run until the'
        ' above errors gets fixed')


def _get_space_separated_linter_name(linter_name):
    """Returns the space separated name of the linter class.

    Args:
        linter_name: str. Name of the linter class.

    Returns:
        str. Space separated name of the linter class.
    """
    return re.sub(
        r'((?<=[a-z])[A-Z]|(?<!\A)[A-Z](?=[a-z]))',
        r' \1', linter_name)


def main(args=None):
    """Main method for pre commit linter script that lints Python, JavaScript,
    HTML, and CSS files.
    """
    parsed_args = _PARSER.parse_args(args=args)
    # File extension to be linted.
    file_extension_types = _get_file_extensions(
        parsed_args.only_check_file_extensions)
    # Default mode is non-verbose mode, if arguments contains --verbose flag it
    # will be made True, which will represent verbose mode.
    verbose_mode_enabled = bool(parsed_args.verbose)
    all_filepaths = _get_all_filepaths(parsed_args.path, parsed_args.files)

    install_third_party_libs.main()
    common.fix_third_party_imports()

    python_utils.PRINT('Starting Linter....')

    if len(all_filepaths) == 0:
        python_utils.PRINT('---------------------------')
        python_utils.PRINT('No files to check.')
        python_utils.PRINT('---------------------------')
        return

    read_files(all_filepaths)
    categorize_files(all_filepaths)

    # Prepare custom tasks.
    custom_max_concurrent_runs = 25
    custom_concurrent_count = min(
        multiprocessing.cpu_count(), custom_max_concurrent_runs)
    custom_semaphore = threading.Semaphore(custom_concurrent_count)

    # Prepare third_party tasks.
    third_party_max_concurrent_runs = 2
    third_party_concurrent_count = min(
        multiprocessing.cpu_count(), third_party_max_concurrent_runs)
    third_party_semaphore = threading.Semaphore(third_party_concurrent_count)

    custom_linters = []
    third_party_linters = []
    for file_extension_type in file_extension_types:
        if (file_extension_type == 'js' or file_extension_type == 'ts'):
            if len(_FILES['.js'] + _FILES['.ts']) == 0:
                continue
        elif (not file_extension_type == 'other' and not
              len(_FILES['.%s' % file_extension_type])):
            continue
        custom_linter, third_party_linter = _get_linters_for_file_extension(
            file_extension_type)
        custom_linters += custom_linter
        third_party_linters += third_party_linter

    # Create tasks.
    tasks_custom = []
    tasks_third_party = []

    for linter in custom_linters:
        name = _get_space_separated_linter_name(type(linter).__name__)
        task_custom = concurrent_task_utils.create_task(
            linter.perform_all_lint_checks, verbose_mode_enabled,
            custom_semaphore, name=name)
        tasks_custom.append(task_custom)

    for linter in third_party_linters:
        name = _get_space_separated_linter_name(type(linter).__name__)
        task_third_party = concurrent_task_utils.create_task(
            linter.perform_all_lint_checks, verbose_mode_enabled,
            third_party_semaphore, name=name)
        tasks_third_party.append(task_third_party)

    # Execute tasks.
    # Here we set Concurrency limit for custom task to 25 because we need to
    # parallelize the tasks to work on full capacity of CPU.
    # Concurrency limit for third party tasks is set to 2 because these
    # third party libraries have their own ways to lint at their fastest
    # (ie. might parallelize on their own)

    # Concurrency limit: 25.
    concurrent_task_utils.execute_tasks(tasks_custom, custom_semaphore)

    # Concurrency limit: 2.
    concurrent_task_utils.execute_tasks(
        tasks_third_party, third_party_semaphore)

    lint_messages = []
    failed = False

    for task in tasks_custom:
        failed = _get_task_output(lint_messages, failed, task)

    for task in tasks_third_party:
        failed = _get_task_output(lint_messages, failed, task)

    errors_stacktrace = concurrent_task_utils.ALL_ERRORS
    if errors_stacktrace:
        _print_errors_stacktrace(errors_stacktrace)

    if failed:
        _print_summary_of_error_messages(lint_messages)
        linter_utils.print_failure_message('\n'.join([
            '---------------------------',
            'Checks Not Passed.',
            '---------------------------']))
        sys.exit(1)
    else:
        linter_utils.print_success_message('\n'.join([
            '---------------------------',
            'All Checks Passed.',
            '---------------------------']))


NAME_SPACE = multiprocessing.Manager().Namespace()
PROCESSES = multiprocessing.Manager().dict()
NAME_SPACE.files = FileCache()
FILE_CACHE = NAME_SPACE.files


# The 'no coverage' pragma is used as this line is un-testable. This is because
# it will only be called when pre_commit_linter.py is used as a
# script.
if __name__ == '__main__': # pragma: no cover
    main()

scripts/linters/pre_commit_linter.py
# coding: utf-8
#
# Copyright 2014 The Oppia Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS-IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Pre-commit script for Oppia.

This script lints Python and JavaScript code, and prints a
list of lint errors to the terminal. If the directory path is passed,
it will lint all Python and JavaScript files in that directory; otherwise,
it will only lint files that have been touched in this commit.

This script ignores all filepaths contained within .eslintignore.

=====================
CUSTOMIZATION OPTIONS
=====================
1.  To lint only files that have been touched in this commit
        python -m scripts.linters.pre_commit_linter

2.  To lint all files in the folder or to lint just a specific file
        python -m scripts.linters.pre_commit_linter --path filepath

3.  To lint a specific list of files. Separate filepaths by spaces
        python -m scripts.linters.pre_commit_linter
            --files filepath_1 filepath_2 ... filepath_n

4.  To lint files in verbose mode
        python -m scripts.linters.pre_commit_linter --verbose

5. To lint a specific list of file extensions. Separate file
    extensions by spaces
        python -m scripts.linters.pre_commit_linter
            --only-check-file-extensions py js

Note that the root folder MUST be named 'oppia'.
 """

from __future__ import absolute_import  # pylint: disable=import-only-modules
from __future__ import unicode_literals  # pylint: disable=import-only-modules

import argparse
import fnmatch
import multiprocessing
import os
import re
import subprocess
import sys
import threading

import python_utils

# Install third party dependencies before proceeding.
from . import codeowner_linter
from . import css_linter
from . import general_purpose_linter
from . import html_linter
from . import js_ts_linter
from . import linter_utils
from . import other_files_linter
from . import python_linter
from .. import common
from .. import concurrent_task_utils
from .. import install_third_party_libs

_PARSER = argparse.ArgumentParser()
_EXCLUSIVE_GROUP = _PARSER.add_mutually_exclusive_group()
_PARSER.add_argument(
    '--path',
    help='path to the directory with files to be linted',
    action='store')
_EXCLUSIVE_GROUP.add_argument(
    '--files',
    nargs='+',
    help='specific files to be linted. Space separated list',
    action='store')
_EXCLUSIVE_GROUP.add_argument(
    '--verbose',
    help='verbose mode. All details will be printed.',
    action='store_true')
_PARSER.add_argument(
    '--only-check-file-extensions',
    nargs='+',
    choices=['html', 'css', 'js', 'ts', 'py', 'other'],
    help='specific file extensions to be linted. Space separated list. '
    'If either of js or ts used then both js and ts files will be linted.',
    action='store')

_PARENT_DIR = os.path.abspath(os.path.join(os.getcwd(), os.pardir))

_PATHS_TO_INSERT = [
    os.getcwd(),
    os.path.join(
        common.GOOGLE_APP_ENGINE_SDK_HOME, 'lib', 'yaml-3.10'),
    os.path.join(
        common.GOOGLE_APP_ENGINE_SDK_HOME, 'lib', 'jinja2-2.6'),
    os.path.join(
        common.GOOGLE_APP_ENGINE_SDK_HOME),
    os.path.join(
        _PARENT_DIR, 'oppia_tools', 'webtest-%s' % common.WEBTEST_VERSION),
    os.path.join(
        _PARENT_DIR, 'oppia_tools', 'PyGithub-%s' % common.PYGITHUB_VERSION),
    os.path.join(
        _PARENT_DIR, 'oppia_tools',
        'setuptools-%s' % common.SETUPTOOLS_VERSION),
    os.path.join(
        _PARENT_DIR, 'oppia_tools', 'Pillow-%s' % common.PILLOW_VERSION),
    os.path.join(
        _PARENT_DIR, 'oppia_tools', 'psutil-%s' % common.PSUTIL_VERSION),
    os.path.join(
        _PARENT_DIR, 'oppia_tools', 'pip-tools-%s' % common.PIP_TOOLS_VERSION),
    os.path.join(
        _PARENT_DIR, 'oppia_tools',
        'simple-crypt-%s' % common.SIMPLE_CRYPT_VERSION),
    common.THIRD_PARTY_PYTHON_LIBS_DIR
]

for path in _PATHS_TO_INSERT:
    sys.path.insert(0, path)

_TARGET_STDOUT = python_utils.string_io()
_STDOUT_LIST = multiprocessing.Manager().list()
_FILES = multiprocessing.Manager().dict()


class FileCache(python_utils.OBJECT):
    """Provides thread-safe access to cached file content."""

    def __init__(self):
        self._CACHE_DATA_DICT = {}

    def read(self, filepath, mode='r'):
        """Returns the data read from the file in unicode form.

        Args:
            filepath: str. The file path from which data is to be read.
            mode: str. The mode in which the file is to be opened.

        Returns:
            str. The data read from the file.
        """
        return self._get_data(filepath, mode)[0]

    def readlines(self, filepath, mode='r'):
        """Returns the tuple containing data line by line as read from the
        file in unicode form.

        Args:
            filepath: str. The file path from which data is to be read.
            mode: str. The mode in which the file is to be opened.

        Returns:
            tuple(str). The tuple containing data line by line as read from the
            file.
        """
        return self._get_data(filepath, mode)[1]

    def _get_data(self, filepath, mode):
        """Returns the collected data from the file corresponding to the given
        filepath.

        Args:
            filepath: str. The file path from which data is to be read.
            mode: str. The mode in which the file is to be opened.

        Returns:
            tuple(str, tuple(str)). The tuple containing data read from the file
            as first element and tuple containing the text line by line as
            second element.
        """
        key = (filepath, mode)
        if key not in self._CACHE_DATA_DICT:
            with python_utils.open_file(filepath, mode, newline='') as f:
                lines = f.readlines()
                self._CACHE_DATA_DICT[key] = (''.join(lines), tuple(lines))
        return self._CACHE_DATA_DICT[key]


def _get_linters_for_file_extension(file_extension_to_lint):
    """Return linters for the file extension type.

    Args:
        file_extension_to_lint: str. The file extension to be linted.

    Returns:
        (CustomLintChecks, ThirdPartyLintChecks). A 2-tuple containing objects
        of lint check classes to run in parallel processing.
    """
    parent_dir = os.path.abspath(os.path.join(os.getcwd(), os.pardir))
    custom_linters = []
    third_party_linters = []

    file_extension_type_js_ts = file_extension_to_lint == 'js' or (
        file_extension_to_lint == 'ts')

    if file_extension_type_js_ts:
        general_files_to_lint = _FILES['.js'] + _FILES['.ts']
    elif file_extension_to_lint == 'other':
        general_files_to_lint = _FILES['other']
    else:
        general_files_to_lint = _FILES['.%s' % file_extension_to_lint]

    custom_linter, third_party_linter = general_purpose_linter.get_linters(
        general_files_to_lint, FILE_CACHE)
    custom_linters.append(custom_linter)

    if file_extension_type_js_ts:
        custom_linter, third_party_linter = js_ts_linter.get_linters(
            _FILES['.js'], _FILES['.ts'], FILE_CACHE)
        custom_linters.append(custom_linter)
        third_party_linters.append(third_party_linter)

    elif file_extension_to_lint == 'html':
        custom_linter, third_party_linter = html_linter.get_linters(
            _FILES['.html'], FILE_CACHE)
        custom_linters.append(custom_linter)
        third_party_linters.append(third_party_linter)

        config_path_for_css_in_html = os.path.join(
            parent_dir, 'oppia', '.stylelintrc')
        custom_linter, third_party_linter = css_linter.get_linters(
            config_path_for_css_in_html, _FILES['.html'])
        third_party_linters.append(third_party_linter)

    elif file_extension_to_lint == 'css':
        config_path_for_oppia_css = os.path.join(
            parent_dir, 'oppia', 'core', 'templates', 'css', '.stylelintrc')
        custom_linter, third_party_linter = css_linter.get_linters(
            config_path_for_oppia_css, _FILES['.css'])
        third_party_linters.append(third_party_linter)

    elif file_extension_to_lint == 'py':
        custom_linter, third_party_linter = python_linter.get_linters(
            _FILES['.py'], FILE_CACHE)
        custom_linters.append(custom_linter)
        third_party_linters.append(third_party_linter)

    elif file_extension_to_lint == 'other':
        custom_linter, _ = codeowner_linter.get_linters(FILE_CACHE)
        custom_linters.append(custom_linter)

        custom_linter, _ = other_files_linter.get_linters(FILE_CACHE)
        custom_linters.append(custom_linter)

    return custom_linters, third_party_linters


def _get_changed_filepaths():
    """Returns a list of modified files (both staged and unstaged)

    Returns:
        list. A list of filepaths of modified files.
    """
    unstaged_files = subprocess.check_output([
        'git', 'diff', '--name-only',
        '--diff-filter=ACM']).splitlines()
    staged_files = subprocess.check_output([
        'git', 'diff', '--cached', '--name-only',
        '--diff-filter=ACM']).splitlines()
    all_changed_filepaths = unstaged_files + staged_files
    return [filepath for filepath in all_changed_filepaths]


def _get_all_files_in_directory(dir_path, excluded_glob_patterns):
    """Recursively collects all files in directory and
    subdirectories of specified path.

    Args:
        dir_path: str. Path to the folder to be linted.
        excluded_glob_patterns: set(str). Set of all glob patterns
            to be excluded.

    Returns:
        list. A list of files in directory and subdirectories without excluded
        files.
    """
    files_in_directory = []
    for _dir, _, files in os.walk(dir_path):
        for file_name in files:
            filepath = os.path.relpath(
                os.path.join(_dir, file_name), os.getcwd())
            if not any([
                    fnmatch.fnmatch(filepath, gp) for gp in
                    excluded_glob_patterns]):
                files_in_directory.append(filepath)
    return files_in_directory


def _get_file_extensions(file_extensions_to_lint):
    """This function is used to return the file extensions which need to be
    linted and checked.

    Args:
        file_extensions_to_lint: list(str). The list of file extensions to be
            linted and checked.

    Returns:
        list(str). The list of all file extensions
        to be linted and checked.
    """
    all_file_extensions_type = ['js', 'py', 'html', 'css', 'other']

    if file_extensions_to_lint:
        # Check if 'js' and 'ts' both are present in file_extensions_to_lint.
        js_and_ts_is_present = 'js' in file_extensions_to_lint and (
            'ts' in file_extensions_to_lint)

        if js_and_ts_is_present:
            python_utils.PRINT(
                'Please use only one of "js" or "ts", as we do not have '
                'separate linters for JS and TS files. If both these options '
                'are used together, then the JS/TS linter will be run twice.')
            python_utils.PRINT('Exiting...')
            sys.exit(1)

        return set(file_extensions_to_lint)

    return all_file_extensions_type


def _get_all_filepaths(input_path, input_filenames):
    """This function is used to return the filepaths which needs to be linted
    and checked.

    Args:
        input_path: str. The path of the directory to be linted and checked.
        input_filenames: list(str). The list of filenames to be linted and
            checked, ignored if input_path is specified.

    Returns:
        list(str). The list of filepaths to be linted and checked.
    """
    eslintignore_path = os.path.join(os.getcwd(), '.eslintignore')
    if input_path:
        input_path = os.path.join(os.getcwd(), input_path)
        if not os.path.exists(input_path):
            python_utils.PRINT(
                'Could not locate file or directory %s. Exiting.' % input_path)
            python_utils.PRINT('----------------------------------------')
            sys.exit(1)
        if os.path.isfile(input_path):
            all_filepaths = [input_path]
        else:
            excluded_glob_patterns = FILE_CACHE.readlines(eslintignore_path)
            all_filepaths = _get_all_files_in_directory(
                input_path, excluded_glob_patterns)
    elif input_filenames:
        valid_filepaths = []
        invalid_filepaths = []
        for filename in input_filenames:
            if os.path.isfile(filename):
                valid_filepaths.append(filename)
            else:
                invalid_filepaths.append(filename)
        if invalid_filepaths:
            python_utils.PRINT(
                'The following file(s) do not exist: %s\n'
                'Exiting.' % invalid_filepaths)
            sys.exit(1)
        all_filepaths = valid_filepaths
    else:
        all_filepaths = _get_changed_filepaths()
    all_filepaths = [
        filename for filename in all_filepaths if not
        any(fnmatch.fnmatch(filename, pattern) for pattern in(
            general_purpose_linter.EXCLUDED_PATHS))]
    return all_filepaths


def read_files(file_paths):
    """Read all files to be checked and cache them. This will spin off multiple
    threads to increase the efficiency.
    """
    threads = []
    for file_path in file_paths:
        thread = threading.Thread(target=FILE_CACHE.read, args=(file_path,))
        thread.start()
        threads.append(thread)

    for thread in threads:
        thread.join()


def categorize_files(file_paths):
    """Categorize all the files and store them in shared variable _FILES."""
    all_filepaths_dict = {
        '.py': [], '.html': [], '.ts': [], '.js': [], 'other': [], '.css': []
    }
    for file_path in file_paths:
        _, extension = os.path.splitext(file_path)
        if extension in all_filepaths_dict:
            all_filepaths_dict[extension].append(file_path)
        else:
            all_filepaths_dict['other'].append(file_path)
    _FILES.update(all_filepaths_dict)


def _print_summary_of_error_messages(lint_messages):
    """Print summary of linter error messages.

    Args:
        lint_messages: list(str). List of linter error messages.
    """
    if lint_messages != '':
        error_message_lines = [
            '----------------------------------------',
            'Please fix the errors below:',
            '----------------------------------------',
            ] + lint_messages
        linter_utils.print_failure_message('\n'.join(error_message_lines))


def _get_task_output(lint_messages, failed, task):
    """Returns output of running tasks.

    Args:
        lint_messages: list(str). List of summary messages of linter output.
        failed: bool. The boolean to check if lint checks fail or not.
        task: object(TestingTaskSpec). The task object to get output of linter.

    Returns:
        bool. The boolean to check if the lint checks fail or not.
    """
    if task.task_results:
        for task_result in task.task_results:
            lint_messages += task_result.trimmed_messages
            if task_result.failed:
                failed = True
    return failed


def _print_errors_stacktrace(errors_stacktrace):
    """Print errors stacktrace caught during linter execution.

    Args:
        errors_stacktrace: list(str). List of error stacktrace of lint
            execution failure.
    """
    python_utils.PRINT('')
    python_utils.PRINT(
        'Unable to run the complete lint test, please check '
        'the following stack trace and fix the errors:')
    python_utils.PRINT('+--------------------------+')
    for stacktrace in errors_stacktrace:
        python_utils.PRINT(stacktrace)
        python_utils.PRINT('--------------------------------------------------')
        python_utils.PRINT('')
    python_utils.PRINT('--------------------------------------------------')
    python_utils.PRINT(
        'Some of the linting functions may not run until the'
        ' above errors gets fixed')


def _get_space_separated_linter_name(linter_name):
    """Returns the space separated name of the linter class.

    Args:
        linter_name: str. Name of the linter class.

    Returns:
        str. Space separated name of the linter class.
    """
    return re.sub(
        r'((?<=[a-z])[A-Z]|(?<!\A)[A-Z](?=[a-z]))',
        r' \1', linter_name)


def main(args=None):
    """Main method for pre commit linter script that lints Python, JavaScript,
    HTML, and CSS files.
    """
    parsed_args = _PARSER.parse_args(args=args)
    # File extension to be linted.
    file_extension_types = _get_file_extensions(
        parsed_args.only_check_file_extensions)
    # Default mode is non-verbose mode, if arguments contains --verbose flag it
    # will be made True, which will represent verbose mode.
    verbose_mode_enabled = bool(parsed_args.verbose)
    all_filepaths = _get_all_filepaths(parsed_args.path, parsed_args.files)

    install_third_party_libs.main()
    common.fix_third_party_imports()

    python_utils.PRINT('Starting Linter....')

    if len(all_filepaths) == 0:
        python_utils.PRINT('---------------------------')
        python_utils.PRINT('No files to check.')
        python_utils.PRINT('---------------------------')
        return

    read_files(all_filepaths)
    categorize_files(all_filepaths)

    # Prepare custom tasks.
    custom_max_concurrent_runs = 25
    custom_concurrent_count = min(
        multiprocessing.cpu_count(), custom_max_concurrent_runs)
    custom_semaphore = threading.Semaphore(custom_concurrent_count)

    # Prepare third_party tasks.
    third_party_max_concurrent_runs = 2
    third_party_concurrent_count = min(
        multiprocessing.cpu_count(), third_party_max_concurrent_runs)
    third_party_semaphore = threading.Semaphore(third_party_concurrent_count)

    custom_linters = []
    third_party_linters = []
    for file_extension_type in file_extension_types:
        if (file_extension_type == 'js' or file_extension_type == 'ts'):
            if len(_FILES['.js'] + _FILES['.ts']) == 0:
                continue
        elif (not file_extension_type == 'other' and not
              len(_FILES['.%s' % file_extension_type])):
            continue
        custom_linter, third_party_linter = _get_linters_for_file_extension(
            file_extension_type)
        custom_linters += custom_linter
        third_party_linters += third_party_linter

    # Create tasks.
    tasks_custom = []
    tasks_third_party = []

    for linter in custom_linters:
        name = _get_space_separated_linter_name(type(linter).__name__)
        task_custom = concurrent_task_utils.create_task(
            linter.perform_all_lint_checks, verbose_mode_enabled,
            custom_semaphore, name=name)
        tasks_custom.append(task_custom)

    for linter in third_party_linters:
        name = _get_space_separated_linter_name(type(linter).__name__)
        task_third_party = concurrent_task_utils.create_task(
            linter.perform_all_lint_checks, verbose_mode_enabled,
            third_party_semaphore, name=name)
        tasks_third_party.append(task_third_party)

    # Execute tasks.
    # Here we set Concurrency limit for custom task to 25 because we need to
    # parallelize the tasks to work on full capacity of CPU.
    # Concurrency limit for third party tasks is set to 2 because these
    # third party libraries have their own ways to lint at their fastest
    # (ie. might parallelize on their own)

    # Concurrency limit: 25.
    concurrent_task_utils.execute_tasks(tasks_custom, custom_semaphore)

    # Concurrency limit: 2.
    concurrent_task_utils.execute_tasks(
        tasks_third_party, third_party_semaphore)

    lint_messages = []
    failed = False

    for task in tasks_custom:
        failed = _get_task_output(lint_messages, failed, task)

    for task in tasks_third_party:
        failed = _get_task_output(lint_messages, failed, task)

    errors_stacktrace = concurrent_task_utils.ALL_ERRORS
    if errors_stacktrace:
        failed = True
        _print_errors_stacktrace(errors_stacktrace)

    if failed:
        _print_summary_of_error_messages(lint_messages)
        linter_utils.print_failure_message('\n'.join([
            '---------------------------',
            'Checks Not Passed.',
            '---------------------------']))
        sys.exit(1)
    else:
        linter_utils.print_success_message('\n'.join([
            '---------------------------',
            'All Checks Passed.',
            '---------------------------']))


NAME_SPACE = multiprocessing.Manager().Namespace()
PROCESSES = multiprocessing.Manager().dict()
NAME_SPACE.files = FileCache()
FILE_CACHE = NAME_SPACE.files


# The 'no coverage' pragma is used as this line is un-testable. This is because
# it will only be called when pre_commit_linter.py is used as a
# script.
if __name__ == '__main__': # pragma: no cover
    main()

scripts/linters/pre_commit_linter.py
diff --git a/.gitignore b/.gitignore
index 0bfa156657f42..b0eab9d79e592 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,6 +21,7 @@ node_modules/*
 coverage.xml
 .viminfo
 .vscode/*
+.nyc_output/*
 libpeerconnection.log
 readme_test_dir/*
 tsc_output_log.txt
diff --git a/package.json b/package.json
index 4ad00697e5dde..2b5e067b7312a 100644
--- a/package.json
+++ b/package.json
@@ -81,6 +81,7 @@
     "karma-webpack": "^4.0.0-rc.3",
     "loader-utils": "^2.0.0",
     "mocha": "^8.1.1",
+    "nyc": "^15.1.0",
     "protractor": "^7.0.0",
     "protractor-jasmine2-screenshot-reporter": "^0.5.0",
     "puppeteer": "^5.0.0",
diff --git a/scripts/linters/custom_eslint_checks/rules/break-after-parens.js b/scripts/linters/custom_eslint_checks/rules/break-after-parens.js
index f29f7380a0680..350f6ad1bc064 100644
--- a/scripts/linters/custom_eslint_checks/rules/break-after-parens.js
+++ b/scripts/linters/custom_eslint_checks/rules/break-after-parens.js
@@ -75,7 +75,7 @@ module.exports = {
           }
           if (paren.value === '(') {
             parensCount += 1;
-          } else if (paren.value === ')') {
+          } else {
             if (parensCount > 0) {
               parensCount -= 1;
             }
diff --git a/scripts/linters/custom_eslint_checks/rules/break-after-parens.spec.js b/scripts/linters/custom_eslint_checks/rules/break-after-parens.spec.js
index cd734c99f2c6e..4f394006070d8 100644
--- a/scripts/linters/custom_eslint_checks/rules/break-after-parens.spec.js
+++ b/scripts/linters/custom_eslint_checks/rules/break-after-parens.spec.js
@@ -32,7 +32,9 @@ ruleTester.run('break-after-parens', rule, {
     `it('should' +
     'happen')`,
     `angular.module('oppia').constant('default',
-    false);`
+    false);`,
+    `var a = (
+      true);`
   ],
 
   invalid: [
diff --git a/scripts/linters/custom_eslint_checks/rules/dependency-checks.js b/scripts/linters/custom_eslint_checks/rules/dependency-checks.js
index 8b5dd31a68a52..2040cab6710ac 100644
--- a/scripts/linters/custom_eslint_checks/rules/dependency-checks.js
+++ b/scripts/linters/custom_eslint_checks/rules/dependency-checks.js
@@ -41,16 +41,6 @@ module.exports = {
     const sourceCode = context.getSourceCode();
 
     var isEquals = function(sortedImports, params) {
-      if (sortedImports === params) {
-        return true;
-      }
-      if (sortedImports === null || params === null) {
-        return false;
-      }
-      if (sortedImports.length !== params.length) {
-        return false;
-      }
-
       for (var i = 0; i < sortedImports.length; ++i) {
         if (sortedImports[i] !== params[i]) {
           return false;
@@ -105,38 +95,41 @@ module.exports = {
       ArrayExpression: function checkDirective(node) {
         var paramsList = [];
         var tokensList = [];
-        if (node.parent.key) {
-          if (node.parent.key.name === 'controller' &&
-              node.parent.parent.type === 'ObjectExpression') {
-            if (node.elements[node.elements.length - 1].type ===
-                'FunctionExpression') {
-              var params = node.elements[node.elements.length - 1].params;
-              for (var index in params) {
-                var param = params[index];
-                paramsList.push(param.name);
+        if (!node.parent.key) {
+          return true;
+        }
+        if (!(node.parent.key.name === 'controller' &&
+            node.parent.parent.type === 'ObjectExpression')) {
+          return true;
+        }
+        if (!(node.elements[node.elements.length - 1].type ===
+            'FunctionExpression')) {
+          return true;
+        }
+        var params = node.elements[node.elements.length - 1].params;
+        for (var index in params) {
+          var param = params[index];
+          paramsList.push(param.name);
+        }
+        var tokens = sourceCode.getTokens(node);
+        tokens.forEach((token) => {
+          if (token.type !== 'Punctuator') {
+            tokensList.push(token.value);
+          }
+        });
+        checkSortedDependency(node, paramsList);
+        paramsList.forEach((param) => {
+          if (isUnused(param, tokensList)) {
+            context.report({
+              node,
+              loc: node.loc,
+              messageId: 'unusedDirective',
+              data: {
+                dependencyName: param
               }
-              var tokens = sourceCode.getTokens(node);
-              tokens.forEach((token) => {
-                if (token.type !== 'Punctuator') {
-                  tokensList.push(token.value);
-                }
-              });
-              checkSortedDependency(node, paramsList);
-              paramsList.forEach((param) => {
-                if (isUnused(param, tokensList)) {
-                  context.report({
-                    node,
-                    loc: node.loc,
-                    messageId: 'unusedDirective',
-                    data: {
-                      dependencyName: param
-                    }
-                  });
-                }
-              });
-            }
+            });
           }
-        }
+        });
       }
     };
   }
diff --git a/scripts/linters/custom_eslint_checks/rules/dependency-checks.spec.js b/scripts/linters/custom_eslint_checks/rules/dependency-checks.spec.js
index 77736c98389de..a13099fb1bcd5 100644
--- a/scripts/linters/custom_eslint_checks/rules/dependency-checks.spec.js
+++ b/scripts/linters/custom_eslint_checks/rules/dependency-checks.spec.js
@@ -58,6 +58,56 @@ ruleTester.run('no-unused-dependency', rule, {
           }
         ]
       };
+    }`,
+    `function test() {
+      return {
+        controllers: [
+          '$http', '$translate', 'I18nLanguageCodeService',
+          'UserService', 'SUPPORTED_SITE_LANGUAGES',
+          function(
+              $http, $translate, I18nLanguageCodeService,
+              UserService, SUPPORTED_SITE_LANGUAGES) {
+            var ctrl = this;
+            // Changes the language of the translations.
+            var preferencesDataUrl = '/preferenceshandler/data';
+            var siteLanguageUrl = '/save_site_language';
+            ctrl.changeLanguage = function() {
+              $translate.use(ctrl.currentLanguageCode);
+              I18nLanguageCodeService.setI18nLanguageCode(
+                ctrl.currentLanguageCode);
+              UserService.getUserInfoAsync().then(function(userInfo) {
+                if (userInfo.isLoggedIn()) {
+                  $http.put(siteLanguageUrl, {
+                    site_language_code: ctrl.currentLanguageCode
+                  });
+                }
+              });
+            };
+            ctrl.$onInit = function() {
+              ctrl.supportedSiteLanguages = SUPPORTED_SITE_LANGUAGES;
+              ctrl.currentLanguageCode = (
+                $translate.proposedLanguage() || $translate.use());
+              I18nLanguageCodeService.setI18nLanguageCode(
+                ctrl.currentLanguageCode);
+            };
+          }
+        ]
+      };
+    }`,
+    `function test() {
+      return {
+        controller: [
+          (function() {
+            return true;
+          }), 'abc', 'abca'
+        ]
+      };
+    }`,
+    `function test() {
+        a =  [
+          '$http', '$translate', 'I18nLanguageCodeService',
+          'UserService', 'SUPPORTED_SITE_LANGUAGES'
+        ]
     }`
   ],
 
diff --git a/scripts/linters/custom_eslint_checks/rules/test-message-style.js b/scripts/linters/custom_eslint_checks/rules/test-message-style.js
index 572c93e3b7bc6..7649bcc267952 100644
--- a/scripts/linters/custom_eslint_checks/rules/test-message-style.js
+++ b/scripts/linters/custom_eslint_checks/rules/test-message-style.js
@@ -75,10 +75,10 @@ module.exports = {
     };
 
     var extractMessage = function(node) {
-      if (node.type === 'Literal') {
-        return node.value;
-      } else if (node.type === 'BinaryExpression') {
+      if (node.type === 'BinaryExpression') {
         return extractMessage(node.left) + extractMessage(node.right);
+      } else {
+        return node.value;
       }
     };
 
diff --git a/scripts/linters/pre_commit_linter.py b/scripts/linters/pre_commit_linter.py
index 9dc0aee9191ea..c852327b45239 100644
--- a/scripts/linters/pre_commit_linter.py
+++ b/scripts/linters/pre_commit_linter.py
@@ -572,6 +572,7 @@ def main(args=None):
 
     errors_stacktrace = concurrent_task_utils.ALL_ERRORS
     if errors_stacktrace:
+        failed = True
         _print_errors_stacktrace(errors_stacktrace)
 
     if failed:
diff --git a/scripts/run_custom_eslint_tests.py b/scripts/run_custom_eslint_tests.py
index 118067fa50bcb..3a8d3ff205f36 100644
--- a/scripts/run_custom_eslint_tests.py
+++ b/scripts/run_custom_eslint_tests.py
@@ -20,6 +20,7 @@
 from __future__ import unicode_literals  # pylint: disable=import-only-modules
 
 import os
+import re
 import subprocess
 import sys
 
@@ -30,9 +31,10 @@
 def main():
     """Run the tests."""
     node_path = os.path.join(common.NODE_PATH, 'bin', 'node')
+    nyc_path = os.path.join('node_modules', 'nyc', 'bin', 'nyc.js')
     mocha_path = os.path.join('node_modules', 'mocha', 'bin', 'mocha')
     filepath = 'scripts/linters/custom_eslint_checks/rules/'
-    proc_args = [node_path, mocha_path, filepath]
+    proc_args = [node_path, nyc_path, mocha_path, filepath]
 
     proc = subprocess.Popen(
         proc_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
@@ -54,6 +56,15 @@ def main():
         python_utils.PRINT('All tests passed')
         python_utils.PRINT('---------------------------')
 
+    coverage_result = re.search = re.search(
+        r'All files\s*\|\s*(?P<stmts>\S+)\s*\|\s*(?P<branch>\S+)\s*\|\s*'
+        r'(?P<funcs>\S+)\s*\|\s*(?P<lines>\S+)\s*\|\s*', tests_stdout)
+    if (coverage_result.group('stmts') != '100' or
+            coverage_result.group('branch') != '100' or
+            coverage_result.group('funcs') != '100' or
+            coverage_result.group('lines') != '100'):
+        raise Exception('Eslint test coverage is not 100%')
+
 
 if __name__ == '__main__':
     main()
diff --git a/yarn.lock b/yarn.lock
index 31c9ef1fbd841..167f3cfd87906 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -300,6 +300,17 @@
     minimatch "^3.0.4"
     strip-json-comments "^3.1.1"
 
+"@istanbuljs/load-nyc-config@^1.0.0":
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
+  integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==
+  dependencies:
+    camelcase "^5.3.1"
+    find-up "^4.1.0"
+    get-package-type "^0.1.0"
+    js-yaml "^3.13.1"
+    resolve-from "^5.0.0"
+
 "@istanbuljs/schema@^0.1.2":
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd"
@@ -1344,11 +1355,23 @@ anymatch@~3.1.1:
     normalize-path "^3.0.0"
     picomatch "^2.0.4"
 
+append-transform@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-2.0.0.tgz#99d9d29c7b38391e6f428d28ce136551f0b77e12"
+  integrity sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==
+  dependencies:
+    default-require-extensions "^3.0.0"
+
 aproba@^1.0.3, aproba@^1.1.1:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
   integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
 
+archy@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40"
+  integrity sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=
+
 are-we-there-yet@~1.1.2:
   version "1.1.5"
   resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21"
@@ -2093,6 +2116,16 @@ cacheable-request@^6.0.0:
     normalize-url "^4.1.0"
     responselike "^1.0.2"
 
+caching-transform@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/caching-transform/-/caching-transform-4.0.0.tgz#00d297a4206d71e2163c39eaffa8157ac0651f0f"
+  integrity sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==
+  dependencies:
+    hasha "^5.0.0"
+    make-dir "^3.0.0"
+    package-hash "^4.0.0"
+    write-file-atomic "^3.0.0"
+
 call-bind@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.0.tgz#24127054bb3f9bdcb4b1fb82418186072f77b8ce"
@@ -2796,7 +2829,7 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5:
     shebang-command "^1.2.0"
     which "^1.2.9"
 
-cross-spawn@^7.0.2:
+cross-spawn@^7.0.0, cross-spawn@^7.0.2:
   version "7.0.3"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
   integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
@@ -3342,6 +3375,13 @@ default-passive-events@^2.0.0:
   resolved "https://registry.yarnpkg.com/default-passive-events/-/default-passive-events-2.0.0.tgz#79b1aa67becbaab38b718469b5480fef92eda649"
   integrity sha512-eMtt76GpDVngZQ3ocgvRcNCklUMwID1PaNbCNxfpDXuiOXttSh0HzBbda1HU9SIUsDc02vb7g9+3I5tlqe/qMQ==
 
+default-require-extensions@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-3.0.0.tgz#e03f93aac9b2b6443fc52e5e4a37b3ad9ad8df96"
+  integrity sha512-ek6DpXq/SCpvjhpFsLFRVtIxJCRw6fUR42lYMVZuUMK7n8eMz4Uh5clckdBjEpLhn/gEBZo7hDJnJcwdKLKQjg==
+  dependencies:
+    strip-bom "^4.0.0"
+
 defer-to-connect@^1.0.1:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591"
@@ -3863,6 +3903,11 @@ es-to-primitive@^1.2.1:
     is-date-object "^1.0.1"
     is-symbol "^1.0.2"
 
+es6-error@^4.0.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d"
+  integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==
+
 es6-promise@^4.0.3:
   version "4.2.8"
   resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
@@ -4410,7 +4455,7 @@ find-cache-dir@^2.1.0:
     make-dir "^2.0.0"
     pkg-dir "^3.0.0"
 
-find-cache-dir@^3.0.0, find-cache-dir@^3.3.1:
+find-cache-dir@^3.0.0, find-cache-dir@^3.2.0, find-cache-dir@^3.3.1:
   version "3.3.1"
   resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.1.tgz#89b33fad4a4670daa94f855f7fbe31d6d84fe880"
   integrity sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==
@@ -4512,6 +4557,14 @@ for-own@^1.0.0:
   dependencies:
     for-in "^1.0.1"
 
+foreground-child@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-2.0.0.tgz#71b32800c9f15aa8f2f83f4a6bd9bff35d861a53"
+  integrity sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==
+  dependencies:
+    cross-spawn "^7.0.0"
+    signal-exit "^3.0.2"
+
 forever-agent@~0.6.1:
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
@@ -4561,6 +4614,11 @@ from@~0:
   resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe"
   integrity sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=
 
+fromentries@^1.2.0:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/fromentries/-/fromentries-1.3.1.tgz#b14c6d4d606c771ce85597f13794fb10200a0ccc"
+  integrity sha512-w4t/zm2J+uAcrpeKyW0VmYiIs3aqe/xKQ+2qwazVNZSCklQHhaVjk6XzKw5GtImq5thgL0IVRjGRAOastb08RQ==
+
 fs-constants@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
@@ -4684,6 +4742,11 @@ get-intrinsic@^1.0.0:
     has "^1.0.3"
     has-symbols "^1.0.1"
 
+get-package-type@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a"
+  integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==
+
 get-pixels-frame-info-update@^3.3.2:
   version "3.3.2"
   resolved "https://registry.yarnpkg.com/get-pixels-frame-info-update/-/get-pixels-frame-info-update-3.3.2.tgz#8b549efcb570454094e5a9dc51d61cb9a62cdb4f"
@@ -5053,6 +5116,14 @@ hash.js@^1.0.0, hash.js@^1.0.3:
     inherits "^2.0.3"
     minimalistic-assert "^1.0.1"
 
+hasha@^5.0.0:
+  version "5.2.2"
+  resolved "https://registry.yarnpkg.com/hasha/-/hasha-5.2.2.tgz#a48477989b3b327aea3c04f53096d816d97522a1"
+  integrity sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==
+  dependencies:
+    is-stream "^2.0.0"
+    type-fest "^0.8.0"
+
 hat@0.0.3:
   version "0.0.3"
   resolved "https://registry.yarnpkg.com/hat/-/hat-0.0.3.tgz#bb014a9e64b3788aed8005917413d4ff3d502d8a"
@@ -5834,6 +5905,11 @@ is-stream@^1.0.1, is-stream@^1.1.0:
   resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
   integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
 
+is-stream@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3"
+  integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==
+
 is-supported-regexp-flag@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/is-supported-regexp-flag/-/is-supported-regexp-flag-1.0.1.tgz#21ee16518d2c1dd3edd3e9a0d57e50207ac364ca"
@@ -5960,11 +6036,18 @@ istanbul-lib-coverage@^2.0.5:
   resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49"
   integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==
 
-istanbul-lib-coverage@^3.0.0:
+istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.0.0-alpha.1:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz#f5944a37c70b550b02a78a5c3b2055b280cec8ec"
   integrity sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==
 
+istanbul-lib-hook@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz#8f84c9434888cc6b1d0a9d7092a76d239ebf0cc6"
+  integrity sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==
+  dependencies:
+    append-transform "^2.0.0"
+
 istanbul-lib-instrument@^1.7.3:
   version "1.10.2"
   resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz#1f55ed10ac3c47f2bdddd5307935126754d0a9ca"
@@ -5978,7 +6061,7 @@ istanbul-lib-instrument@^1.7.3:
     istanbul-lib-coverage "^1.2.1"
     semver "^5.3.0"
 
-istanbul-lib-instrument@^4.0.1:
+istanbul-lib-instrument@^4.0.0, istanbul-lib-instrument@^4.0.1:
   version "4.0.3"
   resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz#873c6fff897450118222774696a3f28902d77c1d"
   integrity sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==
@@ -5988,6 +6071,19 @@ istanbul-lib-instrument@^4.0.1:
     istanbul-lib-coverage "^3.0.0"
     semver "^6.3.0"
 
+istanbul-lib-processinfo@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.2.tgz#e1426514662244b2f25df728e8fd1ba35fe53b9c"
+  integrity sha512-kOwpa7z9hme+IBPZMzQ5vdQj8srYgAtaRqeI48NGmAQ+/5yKiHLV0QbYqQpxsdEF0+w14SoB8YbnHKcXE2KnYw==
+  dependencies:
+    archy "^1.0.0"
+    cross-spawn "^7.0.0"
+    istanbul-lib-coverage "^3.0.0-alpha.1"
+    make-dir "^3.0.0"
+    p-map "^3.0.0"
+    rimraf "^3.0.0"
+    uuid "^3.3.3"
+
 istanbul-lib-report@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6"
@@ -6598,6 +6694,11 @@ locate-path@^6.0.0:
   dependencies:
     p-locate "^5.0.0"
 
+lodash.flattendeep@^4.4.0:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
+  integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=
+
 lodash.get@^4.4.2:
   version "4.4.2"
   resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
@@ -7358,6 +7459,13 @@ node-pre-gyp@^0.11.0:
     semver "^5.3.0"
     tar "^4"
 
+node-preload@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/node-preload/-/node-preload-0.2.1.tgz#c03043bb327f417a18fee7ab7ee57b408a144301"
+  integrity sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==
+  dependencies:
+    process-on-spawn "^1.0.0"
+
 node-releases@^1.1.61:
   version "1.1.65"
   resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.65.tgz#52d9579176bd60f23eba05c4438583f341944b81"
@@ -7497,6 +7605,39 @@ nwsapi@^2.2.0:
   resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7"
   integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==
 
+nyc@^15.1.0:
+  version "15.1.0"
+  resolved "https://registry.yarnpkg.com/nyc/-/nyc-15.1.0.tgz#1335dae12ddc87b6e249d5a1994ca4bdaea75f02"
+  integrity sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==
+  dependencies:
+    "@istanbuljs/load-nyc-config" "^1.0.0"
+    "@istanbuljs/schema" "^0.1.2"
+    caching-transform "^4.0.0"
+    convert-source-map "^1.7.0"
+    decamelize "^1.2.0"
+    find-cache-dir "^3.2.0"
+    find-up "^4.1.0"
+    foreground-child "^2.0.0"
+    get-package-type "^0.1.0"
+    glob "^7.1.6"
+    istanbul-lib-coverage "^3.0.0"
+    istanbul-lib-hook "^3.0.0"
+    istanbul-lib-instrument "^4.0.0"
+    istanbul-lib-processinfo "^2.0.2"
+    istanbul-lib-report "^3.0.0"
+    istanbul-lib-source-maps "^4.0.0"
+    istanbul-reports "^3.0.2"
+    make-dir "^3.0.0"
+    node-preload "^0.2.1"
+    p-map "^3.0.0"
+    process-on-spawn "^1.0.0"
+    resolve-from "^5.0.0"
+    rimraf "^3.0.0"
+    signal-exit "^3.0.2"
+    spawn-wrap "^2.0.0"
+    test-exclude "^6.0.0"
+    yargs "^15.0.2"
+
 oauth-sign@~0.9.0:
   version "0.9.0"
   resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
@@ -7757,6 +7898,13 @@ p-map@^2.0.0:
   resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175"
   integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==
 
+p-map@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/p-map/-/p-map-3.0.0.tgz#d704d9af8a2ba684e2600d9a215983d4141a979d"
+  integrity sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==
+  dependencies:
+    aggregate-error "^3.0.0"
+
 p-map@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b"
@@ -7769,6 +7917,16 @@ p-try@^2.0.0:
   resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
   integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
 
+package-hash@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/package-hash/-/package-hash-4.0.0.tgz#3537f654665ec3cc38827387fc904c163c54f506"
+  integrity sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==
+  dependencies:
+    graceful-fs "^4.1.15"
+    hasha "^5.0.0"
+    lodash.flattendeep "^4.4.0"
+    release-zalgo "^1.0.0"
+
 package-json@^6.3.0:
   version "6.5.0"
   resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0"
@@ -8234,6 +8392,13 @@ process-nextick-args@~2.0.0:
   resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
   integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
 
+process-on-spawn@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/process-on-spawn/-/process-on-spawn-1.0.0.tgz#95b05a23073d30a17acfdc92a440efd2baefdc93"
+  integrity sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==
+  dependencies:
+    fromentries "^1.2.0"
+
 process@^0.11.10:
   version "0.11.10"
   resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
@@ -8639,6 +8804,13 @@ relateurl@^0.2.7:
   resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
   integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=
 
+release-zalgo@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/release-zalgo/-/release-zalgo-1.0.0.tgz#09700b7e5074329739330e535c5a90fb67851730"
+  integrity sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=
+  dependencies:
+    es6-error "^4.0.1"
+
 remark-parse@^8.0.0:
   version "8.0.3"
   resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-8.0.3.tgz#9c62aa3b35b79a486454c690472906075f40c7e1"
@@ -9390,6 +9562,18 @@ sourcemapped-stacktrace@^1.1.11:
   dependencies:
     source-map "0.5.6"
 
+spawn-wrap@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/spawn-wrap/-/spawn-wrap-2.0.0.tgz#103685b8b8f9b79771318827aa78650a610d457e"
+  integrity sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==
+  dependencies:
+    foreground-child "^2.0.0"
+    is-windows "^1.0.2"
+    make-dir "^3.0.0"
+    rimraf "^3.0.0"
+    signal-exit "^3.0.2"
+    which "^2.0.1"
+
 spdx-correct@^3.0.0:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
@@ -9678,6 +9862,11 @@ strip-ansi@^6.0.0:
   dependencies:
     ansi-regex "^5.0.0"
 
+strip-bom@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878"
+  integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==
+
 strip-eof@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
@@ -9985,6 +10174,15 @@ terser@^5.3.4:
     source-map "~0.7.2"
     source-map-support "~0.5.19"
 
+test-exclude@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e"
+  integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==
+  dependencies:
+    "@istanbuljs/schema" "^0.1.2"
+    glob "^7.1.4"
+    minimatch "^3.0.4"
+
 text-table@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
@@ -10278,7 +10476,7 @@ type-fest@^0.6.0:
   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b"
   integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==
 
-type-fest@^0.8.1:
+type-fest@^0.8.0, type-fest@^0.8.1:
   version "0.8.1"
   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
   integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
@@ -10599,7 +10797,7 @@ uuid@^2.0.0:
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a"
   integrity sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=
 
-uuid@^3.3.2:
+uuid@^3.3.2, uuid@^3.3.3:
   version "3.4.0"
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
   integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
@@ -11178,7 +11376,7 @@ yargs@^12.0.5:
     y18n "^3.2.1 || ^4.0.0"
     yargs-parser "^11.1.1"
 
-yargs@^15.1.0, yargs@^15.3.1:
+yargs@^15.0.2, yargs@^15.1.0, yargs@^15.3.1:
   version "15.4.1"
   resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
   integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==

Add test coverage for eslint checks
Add test coverage for eslint checks. See if we can use [nyc](https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&cad=rja&uact=8&ved=2ahUKEwivs4zuvM3rAhUozzgGHTpcCGkQFjACegQIBBAB&url=https%3A%2F%2Fmedium.com%2F%40asemiloore%2Fnodejs-testing-with-mocha-and-code-coverage-with-nyc-9d1d6e428ac1&usg=AOvVaw00Ju9zWuXh4J3QvrWr11Nz)
Add test coverage for eslint checks
Add test coverage for eslint checks. See if we can use [nyc](https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&cad=rja&uact=8&ved=2ahUKEwivs4zuvM3rAhUozzgGHTpcCGkQFjACegQIBBAB&url=https%3A%2F%2Fmedium.com%2F%40asemiloore%2Fnodejs-testing-with-mocha-and-code-coverage-with-nyc-9d1d6e428ac1&usg=AOvVaw00Ju9zWuXh4J3QvrWr11Nz)
