|
1 | 1 | from __future__ import print_function
|
2 | 2 | from __future__ import absolute_import
|
3 | 3 |
|
4 |
| -import os |
5 |
| -import re |
6 |
| -import traceback |
7 |
| - |
8 |
| -from server_lib.printer import print_err, colors |
9 |
| - |
10 |
| -from typing import cast, Any, Callable, Dict, List, Optional, Tuple |
11 |
| - |
12 |
| -def build_custom_checkers(by_lang): |
13 |
| - # type: (Dict[str, List[str]]) -> Tuple[Callable[[], bool], Callable[[], bool]] |
14 |
| - RuleList = List[Dict[str, Any]] |
15 |
| - |
16 |
| - def custom_check_file(fn, identifier, rules, skip_rules=None, max_length=None): |
17 |
| - # type: (str, str, RuleList, Optional[Any], Optional[int]) -> bool |
18 |
| - failed = False |
19 |
| - color = next(colors) |
20 |
| - |
21 |
| - line_tups = [] |
22 |
| - for i, line in enumerate(open(fn)): |
23 |
| - line_newline_stripped = line.strip('\n') |
24 |
| - line_fully_stripped = line_newline_stripped.strip() |
25 |
| - skip = False |
26 |
| - for rule in skip_rules or []: |
27 |
| - if re.match(rule, line): |
28 |
| - skip = True |
29 |
| - if line_fully_stripped.endswith(' # nolint'): |
30 |
| - continue |
31 |
| - if skip: |
32 |
| - continue |
33 |
| - tup = (i, line, line_newline_stripped, line_fully_stripped) |
34 |
| - line_tups.append(tup) |
35 |
| - |
36 |
| - rules_to_apply = [] |
37 |
| - fn_dirname = os.path.dirname(fn) |
38 |
| - for rule in rules: |
39 |
| - exclude_list = rule.get('exclude', set()) |
40 |
| - if fn in exclude_list or fn_dirname in exclude_list: |
41 |
| - continue |
42 |
| - if rule.get("include_only"): |
43 |
| - found = False |
44 |
| - for item in rule.get("include_only", set()): |
45 |
| - if item in fn: |
46 |
| - found = True |
47 |
| - if not found: |
48 |
| - continue |
49 |
| - rules_to_apply.append(rule) |
50 |
| - |
51 |
| - for rule in rules_to_apply: |
52 |
| - exclude_lines = { |
53 |
| - line for |
54 |
| - (exclude_fn, line) in rule.get('exclude_line', set()) |
55 |
| - if exclude_fn == fn |
56 |
| - } |
57 |
| - |
58 |
| - pattern = rule['pattern'] |
59 |
| - for (i, line, line_newline_stripped, line_fully_stripped) in line_tups: |
60 |
| - if line_fully_stripped in exclude_lines: |
61 |
| - exclude_lines.remove(line_fully_stripped) |
62 |
| - continue |
63 |
| - try: |
64 |
| - line_to_check = line_fully_stripped |
65 |
| - if rule.get('strip') is not None: |
66 |
| - if rule['strip'] == '\n': |
67 |
| - line_to_check = line_newline_stripped |
68 |
| - else: |
69 |
| - raise Exception("Invalid strip rule") |
70 |
| - if re.search(pattern, line_to_check): |
71 |
| - print_err(identifier, color, '{} at {} line {}:'.format( |
72 |
| - rule['description'], fn, i+1)) |
73 |
| - print_err(identifier, color, line) |
74 |
| - failed = True |
75 |
| - except Exception: |
76 |
| - print("Exception with %s at %s line %s" % (rule['pattern'], fn, i+1)) |
77 |
| - traceback.print_exc() |
78 |
| - |
79 |
| - if exclude_lines: |
80 |
| - print('Please remove exclusions for file %s: %s' % (fn, exclude_lines)) |
81 |
| - |
82 |
| - lastLine = None |
83 |
| - for (i, line, line_newline_stripped, line_fully_stripped) in line_tups: |
84 |
| - if isinstance(line, bytes): |
85 |
| - line_length = len(line.decode("utf-8")) |
86 |
| - else: |
87 |
| - line_length = len(line) |
88 |
| - if (max_length is not None and line_length > max_length and |
89 |
| - '# type' not in line and 'test' not in fn and 'example' not in fn and |
90 |
| - not re.match("\[[ A-Za-z0-9_:,&()-]*\]: http.*", line) and |
91 |
| - not re.match("`\{\{ external_api_uri_subdomain \}\}[^`]+`", line) and |
92 |
| - "#ignorelongline" not in line and 'migrations' not in fn): |
93 |
| - print("Line too long (%s) at %s line %s: %s" % (len(line), fn, i+1, line_newline_stripped)) |
94 |
| - failed = True |
95 |
| - lastLine = line |
96 |
| - |
97 |
| - if lastLine and ('\n' not in lastLine): |
98 |
| - print("No newline at the end of file. Fix with `sed -i '$a\\' %s`" % (fn,)) |
99 |
| - failed = True |
100 |
| - |
101 |
| - return failed |
102 |
| - |
103 |
| - whitespace_rules = [ |
104 |
| - # This linter should be first since bash_rules depends on it. |
105 |
| - {'pattern': '\s+$', |
106 |
| - 'strip': '\n', |
107 |
| - 'description': 'Fix trailing whitespace'}, |
108 |
| - {'pattern': '\t', |
109 |
| - 'strip': '\n', |
110 |
| - 'description': 'Fix tab-based whitespace'}, |
111 |
| - ] # type: RuleList |
112 |
| - markdown_whitespace_rules = list([rule for rule in whitespace_rules if rule['pattern'] != '\s+$']) + [ |
113 |
| - # Two spaces trailing a line with other content is okay--it's a markdown line break. |
114 |
| - # This rule finds one space trailing a non-space, three or more trailing spaces, and |
115 |
| - # spaces on an empty line. |
116 |
| - {'pattern': '((?<!\s)\s$)|(\s\s\s+$)|(^\s+$)', |
117 |
| - 'strip': '\n', |
118 |
| - 'description': 'Fix trailing whitespace'}, |
119 |
| - {'pattern': '^#+[A-Za-z0-9]', |
120 |
| - 'strip': '\n', |
121 |
| - 'description': 'Missing space after # in heading'}, |
122 |
| - ] # type: RuleList |
123 |
| - python_rules = cast(RuleList, [ |
| 4 | +from typing import cast, Any, Dict, List, Tuple |
| 5 | +from zulint.custom_rules import RuleList |
| 6 | + |
| 7 | +Rule = List[Dict[str, Any]] |
| 8 | + |
| 9 | +whitespace_rules = [ |
| 10 | + # This linter should be first since bash_rules depends on it. |
| 11 | + {'pattern': '\s+$', |
| 12 | + 'strip': '\n', |
| 13 | + 'description': 'Fix trailing whitespace'}, |
| 14 | + {'pattern': '\t', |
| 15 | + 'strip': '\n', |
| 16 | + 'description': 'Fix tab-based whitespace'}, |
| 17 | +] # type: Rule |
| 18 | + |
| 19 | +markdown_whitespace_rules = list([rule for rule in whitespace_rules if rule['pattern'] != '\s+$']) + [ |
| 20 | + # Two spaces trailing a line with other content is okay--it's a markdown line break. |
| 21 | + # This rule finds one space trailing a non-space, three or more trailing spaces, and |
| 22 | + # spaces on an empty line. |
| 23 | + {'pattern': '((?<!\s)\s$)|(\s\s\s+$)|(^\s+$)', |
| 24 | + 'strip': '\n', |
| 25 | + 'description': 'Fix trailing whitespace'}, |
| 26 | + {'pattern': '^#+[A-Za-z0-9]', |
| 27 | + 'strip': '\n', |
| 28 | + 'description': 'Missing space after # in heading'}, |
| 29 | +] # type: Rule |
| 30 | + |
| 31 | +python_rules = RuleList( |
| 32 | + langs=['py'], |
| 33 | + rules=cast(Rule, [ |
124 | 34 | {'pattern': '".*"%\([a-z_].*\)?$',
|
125 | 35 | 'description': 'Missing space around "%"'},
|
126 | 36 | {'pattern': "'.*'%\([a-z_].*\)?$",
|
@@ -186,84 +96,68 @@ def custom_check_file(fn, identifier, rules, skip_rules=None, max_length=None):
|
186 | 96 | 'bad_lines': ['class TestSomeBot(DefaultTests, BotTestCase):'],
|
187 | 97 | 'good_lines': ['class TestSomeBot(BotTestCase, DefaultTests):'],
|
188 | 98 | 'description': 'Bot test cases should inherit from BotTestCase before DefaultTests.'},
|
189 |
| - ]) + whitespace_rules |
190 |
| - bash_rules = [ |
| 99 | + ]) + whitespace_rules, |
| 100 | + max_length=140, |
| 101 | +) |
| 102 | + |
| 103 | +bash_rules = RuleList( |
| 104 | + langs=['sh'], |
| 105 | + rules=cast(Rule, [ |
191 | 106 | {'pattern': '#!.*sh [-xe]',
|
192 | 107 | 'description': 'Fix shebang line with proper call to /usr/bin/env for Bash path, change -x|-e switches'
|
193 | 108 | ' to set -x|set -e'},
|
194 |
| - ] + whitespace_rules[0:1] # type: RuleList |
195 |
| - prose_style_rules = [ |
196 |
| - {'pattern': '[^\/\#\-\"]([jJ]avascript)', # exclude usage in hrefs/divs |
197 |
| - 'description': "javascript should be spelled JavaScript"}, |
198 |
| - {'pattern': '[^\/\-\.\"\'\_\=\>]([gG]ithub)[^\.\-\_\"\<]', # exclude usage in hrefs/divs |
199 |
| - 'description': "github should be spelled GitHub"}, |
200 |
| - {'pattern': '[oO]rganisation', # exclude usage in hrefs/divs |
201 |
| - 'description': "Organization is spelled with a z"}, |
202 |
| - {'pattern': '!!! warning', |
203 |
| - 'description': "!!! warning is invalid; it's spelled '!!! warn'"}, |
204 |
| - {'pattern': '[^-_]botserver(?!rc)|bot server', |
205 |
| - 'description': "Use Botserver instead of botserver or Botserver."}, |
206 |
| - ] # type: RuleList |
207 |
| - json_rules = [] # type: RuleList # fix newlines at ends of files |
208 |
| - # It is okay that json_rules is empty, because the empty list |
209 |
| - # ensures we'll still check JSON files for whitespace. |
210 |
| - markdown_rules = markdown_whitespace_rules + prose_style_rules + [ |
| 109 | + ]) + whitespace_rules[0:1], |
| 110 | +) |
| 111 | + |
| 112 | + |
| 113 | +json_rules = RuleList( |
| 114 | + langs=['json'], |
| 115 | + # Here, we don't check tab-based whitespace, because the tab-based |
| 116 | + # whitespace rule flags a lot of third-party JSON fixtures |
| 117 | + # under zerver/webhooks that we want preserved verbatim. So |
| 118 | + # we just include the trailing whitespace rule and a modified |
| 119 | + # version of the tab-based whitespace rule (we can't just use |
| 120 | + # exclude in whitespace_rules, since we only want to ignore |
| 121 | + # JSON files with tab-based whitespace, not webhook code). |
| 122 | + rules= whitespace_rules[0:1], |
| 123 | +) |
| 124 | + |
| 125 | +prose_style_rules = [ |
| 126 | + {'pattern': '[^\/\#\-\"]([jJ]avascript)', # exclude usage in hrefs/divs |
| 127 | + 'description': "javascript should be spelled JavaScript"}, |
| 128 | + {'pattern': '[^\/\-\.\"\'\_\=\>]([gG]ithub)[^\.\-\_\"\<]', # exclude usage in hrefs/divs |
| 129 | + 'description': "github should be spelled GitHub"}, |
| 130 | + {'pattern': '[oO]rganisation', # exclude usage in hrefs/divs |
| 131 | + 'description': "Organization is spelled with a z"}, |
| 132 | + {'pattern': '!!! warning', |
| 133 | + 'description': "!!! warning is invalid; it's spelled '!!! warn'"}, |
| 134 | + {'pattern': '[^-_]botserver(?!rc)|bot server', |
| 135 | + 'description': "Use Botserver instead of botserver or Botserver."}, |
| 136 | +] # type: Rule |
| 137 | + |
| 138 | +markdown_docs_length_exclude = { |
| 139 | + "zulip_bots/zulip_bots/bots/converter/doc.md", |
| 140 | + "tools/server_lib/README.md", |
| 141 | +} |
| 142 | + |
| 143 | +markdown_rules = RuleList( |
| 144 | + langs=['md'], |
| 145 | + rules=markdown_whitespace_rules + prose_style_rules + cast(Rule, [ |
211 | 146 | {'pattern': '\[(?P<url>[^\]]+)\]\((?P=url)\)',
|
212 | 147 | 'description': 'Linkified markdown URLs should use cleaner <http://example.com> syntax.'}
|
213 |
| - ] |
214 |
| - help_markdown_rules = markdown_rules + [ |
215 |
| - {'pattern': '[a-z][.][A-Z]', |
216 |
| - 'description': "Likely missing space after end of sentence"}, |
217 |
| - {'pattern': '[rR]ealm', |
218 |
| - 'description': "Realms are referred to as Organizations in user-facing docs."}, |
219 |
| - ] |
220 |
| - txt_rules = whitespace_rules |
221 |
| - |
222 |
| - def check_custom_checks_py(): |
223 |
| - # type: () -> bool |
224 |
| - failed = False |
225 |
| - |
226 |
| - for fn in by_lang['py']: |
227 |
| - if 'custom_check.py' in fn: |
228 |
| - continue |
229 |
| - if custom_check_file(fn, 'py', python_rules, max_length=140): |
230 |
| - failed = True |
231 |
| - return failed |
232 |
| - |
233 |
| - def check_custom_checks_nonpy(): |
234 |
| - # type: () -> bool |
235 |
| - failed = False |
236 |
| - |
237 |
| - for fn in by_lang['sh']: |
238 |
| - if custom_check_file(fn, 'sh', bash_rules): |
239 |
| - failed = True |
240 |
| - |
241 |
| - for fn in by_lang['json']: |
242 |
| - if custom_check_file(fn, 'json', json_rules): |
243 |
| - failed = True |
244 |
| - |
245 |
| - markdown_docs_length_exclude = { |
246 |
| - "zulip_bots/zulip_bots/bots/converter/doc.md", |
247 |
| - "tools/server_lib/README.md", |
248 |
| - } |
249 |
| - for fn in by_lang['md']: |
250 |
| - max_length = None |
251 |
| - if fn not in markdown_docs_length_exclude: |
252 |
| - max_length = 120 |
253 |
| - rules = markdown_rules |
254 |
| - if fn.startswith("templates/zerver/help"): |
255 |
| - rules = help_markdown_rules |
256 |
| - if custom_check_file(fn, 'md', rules, max_length=max_length): |
257 |
| - failed = True |
258 |
| - |
259 |
| - for fn in by_lang['txt'] + by_lang['text']: |
260 |
| - if custom_check_file(fn, 'txt', txt_rules): |
261 |
| - failed = True |
262 |
| - |
263 |
| - for fn in by_lang['yaml']: |
264 |
| - if custom_check_file(fn, 'yaml', txt_rules): |
265 |
| - failed = True |
266 |
| - |
267 |
| - return failed |
268 |
| - |
269 |
| - return (check_custom_checks_py, check_custom_checks_nonpy) |
| 148 | + ]), |
| 149 | + max_length=120, |
| 150 | + length_exclude=markdown_docs_length_exclude, |
| 151 | +) |
| 152 | + |
| 153 | +txt_rules = RuleList( |
| 154 | + langs=['txt'], |
| 155 | + rules=whitespace_rules, |
| 156 | +) |
| 157 | + |
| 158 | +non_py_rules = [ |
| 159 | + json_rules, |
| 160 | + markdown_rules, |
| 161 | + bash_rules, |
| 162 | + txt_rules, |
| 163 | +] |
0 commit comments