1# See utils/checkpackagelib/readme.txt before editing this file.
2# Kconfig generates errors if someone introduces a typo like "boool" instead of
3# "bool", so below check functions don't need to check for things already
4# checked by running "make menuconfig".
5
6import re
7
8from checkpackagelib.base import _CheckFunction
9from checkpackagelib.lib import ConsecutiveEmptyLines  # noqa: F401
10from checkpackagelib.lib import EmptyLastLine          # noqa: F401
11from checkpackagelib.lib import NewlineAtEof           # noqa: F401
12from checkpackagelib.lib import TrailingSpace          # noqa: F401
13from checkpackagelib.tool import NotExecutable         # noqa: F401
14
15
16def _empty_or_comment(text):
17    line = text.strip()
18    # ignore empty lines and comment lines indented or not
19    return line == "" or line.startswith("#")
20
21
22def _part_of_help_text(text):
23    return text.startswith("\t  ")
24
25
26# used in more than one check
27entries_that_should_not_be_indented = [
28    "choice", "comment", "config", "endchoice", "endif", "endmenu", "if",
29    "menu", "menuconfig", "source"]
30
31
32class AttributesOrder(_CheckFunction):
33    attributes_order_convention = {
34        "bool": 1, "prompt": 1, "string": 1, "default": 2, "depends": 3,
35        "select": 4, "help": 5}
36
37    def before(self):
38        self.state = 0
39
40    def check_line(self, lineno, text):
41        if _empty_or_comment(text) or _part_of_help_text(text):
42            return
43
44        attribute = text.split()[0]
45
46        if attribute in entries_that_should_not_be_indented:
47            self.state = 0
48            return
49        if attribute not in self.attributes_order_convention.keys():
50            return
51        new_state = self.attributes_order_convention[attribute]
52        wrong_order = self.state > new_state
53
54        # save to process next line
55        self.state = new_state
56
57        if wrong_order:
58            return ["{}:{}: attributes order: type, default, depends on,"
59                    " select, help ({}#_config_files)"
60                    .format(self.filename, lineno, self.url_to_manual),
61                    text]
62
63
64class CommentsMenusPackagesOrder(_CheckFunction):
65    def before(self):
66        self.level = 0
67        self.menu_of_packages = ["The top level menu"]
68        self.new_package = ""
69        self.package = [""]
70        self.print_package_warning = [True]
71        self.state = ""
72
73    def get_level(self):
74        return len(self.state.split('-')) - 1
75
76    def initialize_package_level_elements(self, text):
77        try:
78            self.menu_of_packages[self.level] = text[:-1]
79            self.package[self.level] = ""
80            self.print_package_warning[self.level] = True
81        except IndexError:
82            self.menu_of_packages.append(text[:-1])
83            self.package.append("")
84            self.print_package_warning.append(True)
85
86    def initialize_level_elements(self, text):
87        self.level = self.get_level()
88        self.initialize_package_level_elements(text)
89
90    def check_line(self, lineno, text):
91        # We only want to force sorting for the top-level menus
92        if self.filename not in ["fs/Config.in",
93                                 "package/Config.in",
94                                 "package/Config.in.host",
95                                 "package/kodi/Config.in"]:
96            return
97
98        source_line = re.match(r'^\s*source ".*/([^/]*)/Config.in(.host)?"', text)
99
100        if text.startswith("comment "):
101            if not self.state.endswith("-comment"):
102                self.state += "-comment"
103
104            self.initialize_level_elements(text)
105
106        elif text.startswith("if "):
107            self.state += "-if"
108
109            self.initialize_level_elements(text)
110
111        elif text.startswith("menu "):
112            if self.state.endswith("-comment"):
113                self.state = self.state[:-8]
114
115            self.state += "-menu"
116
117            self.initialize_level_elements(text)
118
119        elif text.startswith("endif") or text.startswith("endmenu"):
120            if self.state.endswith("-comment"):
121                self.state = self.state[:-8]
122
123            if text.startswith("endif"):
124                self.state = self.state[:-3]
125
126            elif text.startswith("endmenu"):
127                self.state = self.state[:-5]
128
129            self.level = self.get_level()
130
131        elif source_line:
132            self.new_package = source_line.group(1)
133
134            # We order _ before A, so replace it with .
135            new_package_ord = self.new_package.replace('_', '.')
136
137            if self.package[self.level] != "" and \
138               self.print_package_warning[self.level] and \
139               new_package_ord < self.package[self.level]:
140                self.print_package_warning[self.level] = False
141                prefix = "{}:{}: ".format(self.filename, lineno)
142                spaces = " " * len(prefix)
143                return ["{prefix}Packages in: {menu},\n"
144                        "{spaces}are not alphabetically ordered;\n"
145                        "{spaces}correct order: '-', '_', digits, capitals, lowercase;\n"
146                        "{spaces}first incorrect package: {package}"
147                        .format(prefix=prefix, spaces=spaces,
148                                menu=self.menu_of_packages[self.level],
149                                package=self.new_package),
150                        text]
151
152            self.package[self.level] = new_package_ord
153
154
155class HelpText(_CheckFunction):
156    HELP_TEXT_FORMAT = re.compile(r"^\t  .{,62}$")
157    URL_ONLY = re.compile(r"^(http|https|git)://\S*$")
158
159    def before(self):
160        self.help_text = False
161
162    def check_line(self, lineno, text):
163        if _empty_or_comment(text):
164            return
165
166        entry = text.split()[0]
167
168        if entry in entries_that_should_not_be_indented:
169            self.help_text = False
170            return
171        if text.strip() == "help":
172            self.help_text = True
173            return
174
175        if not self.help_text:
176            return
177
178        if self.HELP_TEXT_FORMAT.match(text.rstrip()):
179            return
180        if self.URL_ONLY.match(text.strip()):
181            return
182        return ["{}:{}: help text: <tab><2 spaces><62 chars>"
183                " ({}#writing-rules-config-in)"
184                .format(self.filename, lineno, self.url_to_manual),
185                text,
186                "\t  " + "123456789 " * 6 + "12"]
187
188
189class Indent(_CheckFunction):
190    ENDS_WITH_BACKSLASH = re.compile(r"^[^#].*\\$")
191    entries_that_should_be_indented = [
192        "bool", "default", "depends", "help", "prompt", "select", "string"]
193
194    def before(self):
195        self.backslash = False
196
197    def check_line(self, lineno, text):
198        if _empty_or_comment(text) or _part_of_help_text(text):
199            self.backslash = False
200            return
201
202        entry = text.split()[0]
203
204        last_line_ends_in_backslash = self.backslash
205
206        # calculate for next line
207        if self.ENDS_WITH_BACKSLASH.search(text):
208            self.backslash = True
209        else:
210            self.backslash = False
211
212        if last_line_ends_in_backslash:
213            if text.startswith("\t"):
214                return
215            return ["{}:{}: continuation line should be indented using tabs"
216                    .format(self.filename, lineno),
217                    text]
218
219        if entry in self.entries_that_should_be_indented:
220            if not text.startswith("\t{}".format(entry)):
221                return ["{}:{}: should be indented with one tab"
222                        " ({}#_config_files)"
223                        .format(self.filename, lineno, self.url_to_manual),
224                        text]
225        elif entry in entries_that_should_not_be_indented:
226            if not text.startswith(entry):
227                # four Config.in files have a special but legitimate indentation rule
228                if self.filename in ["package/Config.in",
229                                     "package/Config.in.host",
230                                     "package/kodi/Config.in",
231                                     "package/x11r7/Config.in"]:
232                    return
233                return ["{}:{}: should not be indented"
234                        .format(self.filename, lineno),
235                        text]
236
237
238class RedefinedConfig(_CheckFunction):
239    CONFIG = re.compile(r"^\s*(menu|)config\s+(BR2_\w+)\b")
240    IF = re.compile(r"^\s*if\s+([^#]*)\b")
241    ENDIF = re.compile(r"^\s*endif\b")
242
243    def before(self):
244        self.configs = {}
245        self.conditional = []
246
247    def check_line(self, lineno, text):
248        if _empty_or_comment(text) or _part_of_help_text(text):
249            return
250
251        m = self.IF.search(text)
252        if m is not None:
253            condition = m.group(1)
254            self.conditional.append(condition)
255            return
256
257        m = self.ENDIF.search(text)
258        if m is not None:
259            self.conditional.pop()
260            return
261
262        m = self.CONFIG.search(text)
263        if m is None:
264            return
265        config = m.group(2)
266
267        key = (config, ' AND '.join(self.conditional))
268        if key in self.configs.keys():
269            previous_line = self.configs[key]
270            return ["{}:{}: config {} redeclared (previous line: {})"
271                    .format(self.filename, lineno, config, previous_line),
272                    text]
273        self.configs[key] = lineno
274