tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

create.rst (14019B)


      1 Adding a New Linter to the Tree
      2 ===============================
      3 
      4 Linter Requirements
      5 -------------------
      6 
      7 For a linter to be integrated into the mozilla-central tree, it needs to have:
      8 
      9 * Any required dependencies should be installed as part of ``./mach bootstrap``
     10 * A ``./mach lint`` interface
     11 * Running ``./mach lint`` command must pass (note, linters can be disabled for individual directories)
     12 * Taskcluster/Treeherder integration
     13 * In tree documentation (under ``docs/code-quality/lint``) to give a basic summary, links and any other useful information
     14 * Unit tests (under ``tools/lint/test``) to make sure that the linter works as expected and we don't regress.
     15 
     16 The review group in Phabricator is ``#linter-reviewers``.
     17 
     18 Linter Basics
     19 -------------
     20 
     21 A linter is a yaml file with a ``.yml`` extension. Depending on how the type of linter, there may
     22 be python code alongside the definition, pointed to by the 'payload' attribute.
     23 
     24 Here's a trivial example:
     25 
     26 no-eval.yml
     27 
     28 .. code-block:: yaml
     29 
     30    EvalLinter:
     31        description: Ensures the string eval doesn't show up.
     32        extensions: ['js']
     33        type: string
     34        payload: eval
     35 
     36 Now ``no-eval.yml`` gets passed into :func:`LintRoller.read`.
     37 
     38 
     39 Linter Types
     40 ------------
     41 
     42 There are four types of linters, though more may be added in the future.
     43 
     44 1. string - fails if substring is found
     45 2. regex - fails if regex matches
     46 3. external - fails if a python function returns a non-empty result list
     47 4. structured_log - fails if a mozlog logger emits any lint_error or lint_warning log messages
     48 
     49 As seen from the example above, string and regex linters are very easy to create, but they
     50 should be avoided if possible. It is much better to use a context aware linter for the language you
     51 are trying to lint. For example, use eslint to lint JavaScript files, use ruff to lint Python
     52 files, etc.
     53 
     54 Which brings us to the third and most interesting type of linter,
     55 external.  External linters call an arbitrary python function which is
     56 responsible for not only running the linter, but ensuring the results
     57 are structured properly. For example, an external type could shell out
     58 to a 3rd party linter, collect the output and format it into a list of
     59 :class:`Issue` objects. The signature for this python
     60 function is ``lint(files, config, **kwargs)``, where ``files`` is a list of
     61 files to lint and ``config`` is the linter definition defined in the ``.yml``
     62 file.
     63 
     64 Structured log linters are much like external linters, but suitable
     65 for cases where the linter code is using mozlog and emits
     66 ``lint_error`` or ``lint_warning`` logging messages when the lint
     67 fails. This is recommended for writing novel gecko-specific lints. In
     68 this case the signature for lint functions is ``lint(files, config, logger,
     69 **kwargs)``.
     70 
     71 
     72 Linter Definition
     73 -----------------
     74 
     75 Each ``.yml`` file must have at least one linter defined in it. Here are the supported keys:
     76 
     77 * description - A brief description of the linter's purpose (required)
     78 * type - One of 'string', 'regex' or 'external' (required)
     79 * payload - The actual linting logic, depends on the type (required)
     80 * include - A list of file paths that will be considered (optional)
     81 * exclude - A list of file paths or glob patterns that must not be matched (optional)
     82 * extensions - A list of file extensions to be considered (optional)
     83 * exclude_extensions - A list of file extensions to be excluded (optional)
     84 * setup - A function that sets up external dependencies (optional)
     85 * support-files - A list of glob patterns matching configuration files (optional)
     86 * find-dotfiles - If set to ``true``, run on dot files (.*) (optional)
     87 * ignore-case - If set to ``true`` and ``type`` is regex, ignore the case (optional)
     88 
     89 Note that a linter may not have both ``extensions`` and ``exclude_extensions`` specified at the
     90 same time.
     91 
     92 In addition to the above, some ``.yml`` files correspond to a single lint rule. For these, the
     93 following additional keys may be specified:
     94 
     95 * message - A string to print on infraction (optional)
     96 * hint - A string with a clue on how to fix the infraction (optional)
     97 * rule - An id string for the lint rule (optional)
     98 * level - The severity of the infraction, either 'error' or 'warning' (optional)
     99 
    100 For structured_log lints the following additional keys apply:
    101 
    102 * logger - A StructuredLog object to use for logging. If not supplied
    103  one will be created (optional)
    104 
    105 
    106 Example
    107 -------
    108 
    109 Here is an example of an external linter that shells out to the Python ruff linter,
    110 let's call the file ``ruff_lint.py`` (`in-tree version <https://searchfox.org/mozilla-central/source/tools/lint/python/ruff.py>`__):
    111 
    112 .. code-block:: python
    113 
    114    import json
    115    import os
    116    import subprocess
    117    from collections import defaultdict
    118    from shutil import which
    119 
    120    from mozlint import result
    121 
    122 
    123    RUFF_NOT_FOUND = """
    124    Could not find ruff! Install ruff and try again.
    125    """.strip()
    126 
    127 
    128    def lint(paths, config, **lintargs):
    129        binary = which('ruff')
    130        if not binary:
    131            print(RUFF_NOT_FOUND)
    132            return 1
    133 
    134 
    135        cmd = ["ruff", "check", "--force-exclude", "--format=json"] + paths
    136        output = subprocess.run(cmd, stdout=subprocess.PIPE, env=os.environ).output
    137 
    138        # all passed
    139        if not output:
    140            return []
    141 
    142        try:
    143            issues = json.loads(output)
    144        except json.JSONDecodeError:
    145            log.error(f"Could not parse output: {output}")
    146 
    147        results = []
    148        for issue in issues:
    149            # convert ruff's format to mozlint's format
    150            res = {
    151                "path": issue["filename"],
    152                "lineno": issue["location"]["row"],
    153                "column": issue["location"]["column"],
    154                "lineoffset": issue["end_location"]["row"] - issue["location"]["row"],
    155                "message": issue["message"],
    156                "rule": issue["code"],
    157                "level": "error",
    158            }
    159 
    160            if issue["fix"]:
    161                res["hint"] = issue["fix"]["message"]
    162 
    163            results.append(result.from_config(config, **res))
    164 
    165        return {"results": results, "fixed": fixed}
    166 
    167 Now here is the linter definition that would call it:
    168 
    169 .. code-block:: yaml
    170 
    171    ruff:
    172        description: Python Linter
    173        include: ["."]
    174        extensions: ["py"]
    175        support-files:
    176            - "**/.ruff.toml"
    177            - "**/ruff.toml"
    178            - "**/pyproject.toml"
    179        type: external
    180        payload: py.ruff:lint
    181 
    182 Notice the payload has two parts, delimited by ':'. The first is the module
    183 path, which ``mozlint`` will attempt to import. The second is the object path
    184 within that module (e.g, the name of a function to call). It is up to consumers
    185 of ``mozlint`` to ensure the module is in ``sys.path``. Structured log linters
    186 use the same import mechanism.
    187 
    188 The ``support-files`` key is used to list configuration files or files related
    189 to the running of the linter itself. If using ``--outgoing`` or ``--workdir``
    190 and one of these files was modified, the entire tree will be linted instead of
    191 just the modified files.
    192 
    193 Result definition
    194 -----------------
    195 
    196 When generating the list of results, the following values are available.
    197 
    198 .. csv-table::
    199   :header: "Name", "Description", "Optional"
    200   :widths: 20, 40, 10
    201 
    202    "linter", "Name of the linter that flagged this error", ""
    203    "path", "Path to the file containing the error", ""
    204    "message", "Text describing the error", ""
    205    "lineno", "Line number that contains the error", ""
    206    "column", "Column containing the error", ""
    207    "level", "Severity of the error, either 'warning' or 'error' (default 'error')", "Yes"
    208    "hint", "Suggestion for fixing the error", "Yes"
    209    "source", "Source code context of the error", "Yes"
    210    "rule", "Name of the rule that was violated", "Yes"
    211    "lineoffset", "Denotes an error spans multiple lines, of the form (<lineno offset>, <num lines>)", "Yes"
    212    "diff", "A diff describing the changes that need to be made to the code", "Yes"
    213 
    214 
    215 Automated testing
    216 -----------------
    217 
    218 Every new checker must have associated tests. If your linter is ``mylinter`` then the
    219 test file should be named ``tools/lint/test/test_mylinter.py`` and any example files
    220 named like ``tools/lint/test/files/mylinter/my-example-file``. Be sure that your test
    221 has been added as a section ``["test_mylinter.py"]`` in the manifest ``tools/lint/test/python.toml``.
    222 
    223 They should be pretty easy to write as most of the work is managed by the Mozlint
    224 framework. The key declaration is the ``LINTER`` variable which must match
    225 the linker declaration.
    226 
    227 As an example, the `ruff test <https://searchfox.org/mozilla-central/source/tools/lint/test/test_ruff.py>`_ looks like the following snippet:
    228 
    229 .. code-block:: python
    230 
    231    import mozunit
    232    LINTER = 'ruff'
    233 
    234    def test_lint_ruff(lint, paths):
    235        results = lint(paths('bad.py'))
    236        assert len(results) == 2
    237        assert results[0].rule == 'F401'
    238        assert results[1].rule == 'E501'
    239        assert results[1].lineno == 5
    240 
    241    if __name__ == '__main__':
    242        mozunit.main()
    243 
    244 As always with tests, please make sure that enough positive and negative cases
    245 are covered.
    246 
    247 To run the tests:
    248 
    249 .. code-block:: shell
    250 
    251    $ ./mach python-test --subsuite mozlint
    252 
    253 To run a specific test:
    254 
    255 .. code-block:: shell
    256 
    257    ./mach python-test --subsuite mozlint tools/lint/test/test_black.py
    258 
    259 More tests can be `found in-tree <https://searchfox.org/mozilla-central/source/tools/lint/test>`_.
    260 
    261 Tracking fixed issues
    262 ---------------------
    263 
    264 All the linters that provide ``fix support`` returns a dictionary instead of a list.
    265 
    266 ``{"results":result,"fixed":fixed}``
    267 
    268 * results - All the linting errors it was not able to fix
    269 * fixed - Count of fixed errors (for ``fix=False`` this is 0)
    270 
    271 Some linters (example: `codespell <https://searchfox.org/mozilla-central/rev/0379f315c75a2875d716b4f5e1a18bf27188f1e6/tools/lint/spell/__init__.py#145-163>`_) might require two passes to count the number of fixed issues.
    272 Others might just need `some tuning <https://searchfox.org/mozilla-central/rev/0379f315c75a2875d716b4f5e1a18bf27188f1e6/tools/lint/file-whitespace/__init__.py#28,60,85,112>`_.
    273 
    274 For adding tests to check your fixed count, add a global variable ``fixed = 0``
    275 and write a function to add your test as mentioned under ``Automated testing`` section.
    276 
    277 
    278 Here's an example
    279 
    280 .. code-block:: python
    281 
    282    fixed = 0
    283 
    284 
    285    def test_lint_codespell_fix(lint, create_temp_file):
    286    # Typo has been fixed in the contents to avoid triggering warning
    287    # 'informations' ----> 'information'
    288        contents = """This is a file with some typos and information.
    289    But also testing false positive like optin (because this isn't always option)
    290    or stuff related to our coding style like:
    291    aparent (aParent).
    292    but detects mistakes like mozilla
    293    """.lstrip()
    294 
    295        path = create_temp_file(contents, "ignore.rst")
    296        lint([path], fix=True)
    297 
    298        assert fixed == 2
    299 
    300 
    301 Bootstrapping Dependencies
    302 --------------------------
    303 
    304 Many linters, especially 3rd party ones, will require a set of dependencies. It
    305 could be as simple as installing a binary from a package manager, or as
    306 complicated as pulling a whole graph of tools, plugins and their dependencies.
    307 
    308 Either way, to reduce the burden on users, linters should strive to provide
    309 automated bootstrapping of all their dependencies. To help with this,
    310 ``mozlint`` allows linters to define a ``setup`` config, which has the same
    311 path object format as an external payload. For example (`in-tree version <https://searchfox.org/mozilla-central/source/tools/lint/ruff.yml>`__):
    312 
    313 .. code-block:: yaml
    314 
    315    ruff:
    316        description: Python linter
    317        include: ['.']
    318        extensions: ['py']
    319        type: external
    320        payload: py.ruff:lint
    321        setup: py.ruff:setup
    322 
    323 The setup function takes a single argument, the root of the repository being
    324 linted. In the case of ``ruff``, it might look like:
    325 
    326 .. code-block:: python
    327 
    328    import subprocess
    329    from shutil import which
    330 
    331    def setup(root, **lintargs):
    332        # This is a simple example. Please look at the actual source for better examples.
    333        if not which("ruff"):
    334            subprocess.call(["pip", "install", "ruff"])
    335 
    336 The setup function will be called implicitly before running the linter. This
    337 means it should return fast and not produce any output if there is no setup to
    338 be performed.
    339 
    340 The setup functions can also be called explicitly by running ``mach lint
    341 --setup``. This will only perform setup and not perform any linting. It is
    342 mainly useful for other tools like ``mach bootstrap`` to call into.
    343 
    344 
    345 Adding the linter to the CI
    346 ---------------------------
    347 
    348 First, the job will have to be declared in Taskcluster.
    349 
    350 This should be done in the `mozlint Taskcluster configuration <https://searchfox.org/mozilla-central/source/taskcluster/kinds/source-test/mozlint.yml>`_.
    351 You will need to define a symbol, how it is executed and on what kind of change.
    352 
    353 For example, for ruff, the configuration is the following:
    354 
    355 .. code-block:: yaml
    356 
    357    py-ruff:
    358        description: run ruff over the gecko codebase
    359        treeherder:
    360            symbol: py(ruff)
    361        run:
    362            mach: lint -l ruff -f treeherder -f json:/builds/worker/mozlint.json .
    363        when:
    364            files-changed:
    365                - '**/*.py'
    366                - '**/.ruff.toml'
    367 
    368 If the linter requires an external program, you will have to install it in the `setup script <https://searchfox.org/mozilla-central/source/taskcluster/docker/lint/system-setup.sh>`_
    369 and maybe install the necessary files in the `Docker configuration <https://searchfox.org/mozilla-central/source/taskcluster/docker/lint/Dockerfile>`_.
    370 
    371 .. note::
    372 
    373    If the defect found by the linter is minor, make sure that it is logged as
    374    a warning by setting `{"level": "warning"}` in the
    375    :class:`~mozlint.result.Issue`. This means the defect will not cause a
    376    backout if landed, but will still be surfaced by reviewbot at review time,
    377    or when using `-W/--warnings` locally.