summaryrefslogtreecommitdiffstats
path: root/python/mozlint/mozlint/parser.py
blob: eac502495b2fca30c00c2fb7a219bd84b17f6714 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

import os

import yaml

from .errors import LinterNotFound, LinterParseError
from .types import supported_types

GLOBAL_SUPPORT_FILES = []


class Parser(object):
    """Reads and validates lint configuration files."""

    required_attributes = (
        "name",
        "description",
        "type",
        "payload",
    )

    def __init__(self, root):
        self.root = root

    def __call__(self, path):
        return self.parse(path)

    def _validate(self, linter):
        relpath = os.path.relpath(linter["path"], self.root)

        missing_attrs = []
        for attr in self.required_attributes:
            if attr not in linter:
                missing_attrs.append(attr)

        if missing_attrs:
            raise LinterParseError(
                relpath,
                "Missing required attribute(s): " "{}".format(",".join(missing_attrs)),
            )

        if linter["type"] not in supported_types:
            raise LinterParseError(relpath, "Invalid type '{}'".format(linter["type"]))

        for attr in ("include", "exclude", "support-files"):
            if attr not in linter:
                continue

            if not isinstance(linter[attr], list) or not all(
                isinstance(a, str) for a in linter[attr]
            ):
                raise LinterParseError(
                    relpath,
                    "The {} directive must be a " "list of strings!".format(attr),
                )
            invalid_paths = set()
            for path in linter[attr]:
                if "*" in path:
                    if attr == "include":
                        raise LinterParseError(
                            relpath,
                            "Paths in the include directive cannot "
                            "contain globs:\n  {}".format(path),
                        )
                    continue

                abspath = path
                if not os.path.isabs(abspath):
                    abspath = os.path.join(self.root, path)

                if not os.path.exists(abspath):
                    invalid_paths.add("  " + path)

            if invalid_paths:
                raise LinterParseError(
                    relpath,
                    "The {} directive contains the following "
                    "paths that don't exist:\n{}".format(
                        attr, "\n".join(sorted(invalid_paths))
                    ),
                )

        if "setup" in linter:
            if linter["setup"].count(":") != 1:
                raise LinterParseError(
                    relpath,
                    "The setup attribute '{!r}' must have the "
                    "form 'module:object'".format(linter["setup"]),
                )

        if "extensions" in linter:
            linter["extensions"] = [e.strip(".") for e in linter["extensions"]]

    def parse(self, path):
        """Read a linter and return its LINTER definition.

        :param path: Path to the linter.
        :returns: List of linter definitions ([dict])
        :raises: LinterNotFound, LinterParseError
        """
        if not os.path.isfile(path):
            raise LinterNotFound(path)

        if not path.endswith(".yml"):
            raise LinterParseError(
                path, "Invalid filename, linters must end with '.yml'!"
            )

        with open(path) as fh:
            configs = list(yaml.safe_load_all(fh))

        if not configs:
            raise LinterParseError(path, "No lint definitions found!")

        linters = []
        for config in configs:
            for name, linter in config.items():
                linter["name"] = name
                linter["path"] = path
                self._validate(linter)
                linter.setdefault("support-files", []).extend(
                    GLOBAL_SUPPORT_FILES + [path]
                )
                linter.setdefault("include", ["."])
                linters.append(linter)

        return linters