6
6
import pathlib
7
7
import sys
8
8
import traceback
9
+ import typing as t
9
10
10
- from docutils import nodes
11
- from docutils .core import Publisher
12
- from docutils .io import StringInput
13
- from docutils .parsers .rst import Directive
14
- from docutils .parsers .rst .directives import register_directive
15
- from docutils .parsers .rst .directives import unchanged as directive_param_unchanged
16
- from docutils .utils import Reporter , SystemMessage
11
+ from antsibull_docutils .rst_code_finder import find_code_blocks
17
12
from yamllint import linter
18
13
from yamllint .config import YamlLintConfig
19
14
from yamllint .linter import PROBLEM_LEVELS
44
39
}
45
40
46
41
47
- class IgnoreDirective (Directive ):
48
- has_content = True
49
-
50
- def run (self ) -> list :
51
- return []
52
-
53
-
54
- class CodeBlockDirective (Directive ):
55
- has_content = True
56
- optional_arguments = 1
57
-
58
- # These are all options Sphinx allows for code blocks.
59
- # We need to have them here so that docutils successfully parses this extension.
60
- option_spec = {
61
- "caption" : directive_param_unchanged ,
62
- "class" : directive_param_unchanged ,
63
- "dedent" : directive_param_unchanged ,
64
- "emphasize-lines" : directive_param_unchanged ,
65
- "name" : directive_param_unchanged ,
66
- "force" : directive_param_unchanged ,
67
- "linenos" : directive_param_unchanged ,
68
- "lineno-start" : directive_param_unchanged ,
69
- }
70
-
71
- def run (self ) -> list [nodes .literal_block ]:
72
- code = "\n " .join (self .content )
73
- literal = nodes .literal_block (code , code )
74
- literal ["classes" ].append ("code-block" )
75
- literal ["ansible-code-language" ] = self .arguments [0 ] if self .arguments else None
76
- literal ["ansible-code-block" ] = True
77
- literal ["ansible-code-lineno" ] = self .lineno
78
- return [literal ]
79
-
80
-
81
- class YamlLintVisitor (nodes .SparseNodeVisitor ):
82
- def __init__ (
83
- self ,
84
- document : nodes .document ,
85
- path : str ,
86
- results : list [dict ],
87
- content : str ,
88
- yamllint_config : YamlLintConfig ,
89
- ):
90
- super ().__init__ (document )
91
- self .__path = path
92
- self .__results = results
93
- self .__content_lines = content .splitlines ()
94
- self .__yamllint_config = yamllint_config
95
-
96
- def visit_system_message (self , node : nodes .system_message ) -> None :
97
- raise nodes .SkipNode
98
-
99
- def visit_error (self , node : nodes .error ) -> None :
100
- raise nodes .SkipNode
101
-
102
- def visit_literal_block (self , node : nodes .literal_block ) -> None :
103
- if "ansible-code-block" not in node .attributes :
104
- if node .attributes ["classes" ]:
105
- self .__results .append (
106
- {
107
- "path" : self .__path ,
108
- "line" : node .line or "unknown" ,
109
- "col" : 0 ,
110
- "message" : (
111
- "Warning: found unknown literal block! Check for double colons '::'."
112
- " If that is not the cause, please report this warning."
113
- " It might indicate a bug in the checker or an unsupported Sphinx directive."
114
- f" Node: { node !r} ; attributes: { node .attributes } ; content: { node .rawsource !r} "
115
- ),
116
- }
117
- )
118
- raise nodes .SkipNode
119
-
120
- language = node .attributes ["ansible-code-language" ]
121
- lineno = node .attributes ["ansible-code-lineno" ]
122
-
123
- # Ok, we have to find both the row and the column offset for the actual code content
124
- row_offset = lineno
125
- found_empty_line = False
126
- found_content_lines = False
127
- content_lines = node .rawsource .count ("\n " ) + 1
128
- min_indent = None
129
- for offset , line in enumerate (self .__content_lines [lineno :]):
130
- stripped_line = line .strip ()
131
- if not stripped_line :
132
- if not found_empty_line :
133
- row_offset = lineno + offset + 1
134
- found_empty_line = True
135
- elif not found_content_lines :
136
- found_content_lines = True
137
- row_offset = lineno + offset
138
-
139
- if found_content_lines and content_lines > 0 :
140
- if stripped_line :
141
- indent = len (line ) - len (line .lstrip ())
142
- if min_indent is None or min_indent > indent :
143
- min_indent = indent
144
- content_lines -= 1
145
- elif not content_lines :
146
- break
147
-
148
- min_source_indent = None
149
- for line in node .rawsource .split ("\n " ):
150
- stripped_line = line .lstrip ()
151
- if stripped_line :
152
- indent = len (line ) - len (line .lstrip ())
153
- if min_source_indent is None or min_source_indent > indent :
154
- min_source_indent = indent
155
-
156
- col_offset = max (0 , (min_indent or 0 ) - (min_source_indent or 0 ))
157
-
158
- # Now that we have the offsets, we can actually do some processing...
159
- if language not in {"YAML" , "yaml" , "yaml+jinja" , "YAML+Jinja" }:
160
- if language is None :
161
- allowed_languages = ", " .join (sorted (ALLOWED_LANGUAGES ))
162
- self .__results .append (
163
- {
164
- "path" : self .__path ,
165
- "line" : row_offset + 1 ,
166
- "col" : col_offset + 1 ,
167
- "message" : (
168
- "Literal block without language!"
169
- f" Allowed languages are: { allowed_languages } ."
170
- ),
171
- }
172
- )
173
- return
174
- if language not in ALLOWED_LANGUAGES :
175
- allowed_languages = ", " .join (sorted (ALLOWED_LANGUAGES ))
176
- self .__results .append (
177
- {
178
- "path" : self .__path ,
179
- "line" : row_offset + 1 ,
180
- "col" : col_offset + 1 ,
181
- "message" : (
182
- f"Warning: literal block with disallowed language: { language } ."
183
- " If the language should be allowed, the checker needs to be updated."
184
- f" Currently allowed languages are: { allowed_languages } ."
185
- ),
186
- }
187
- )
188
- raise nodes .SkipNode
189
-
190
- # So we have YAML. Let's lint it!
191
- try :
192
- problems = linter .run (
193
- io .StringIO (node .rawsource .rstrip () + "\n " ),
194
- self .__yamllint_config ,
195
- self .__path ,
196
- )
197
- for problem in problems :
198
- if problem .level not in REPORT_LEVELS :
199
- continue
200
- msg = f"{ problem .level } : { problem .desc } "
201
- if problem .rule :
202
- msg += f" ({ problem .rule } )"
203
- self .__results .append (
204
- {
205
- "path" : self .__path ,
206
- "line" : row_offset + problem .line ,
207
- "col" : col_offset + problem .column ,
208
- "message" : msg ,
209
- }
210
- )
211
- except Exception as exc :
212
- error = str (exc ).replace ("\n " , " / " )
213
- self .__results .append (
214
- {
215
- "path" : self .__path ,
216
- "line" : row_offset + 1 ,
217
- "col" : col_offset + 1 ,
218
- "message" : (
219
- f"Internal error while linting YAML: exception { type (exc )} :"
220
- f" { error } ; traceback: { traceback .format_exc ()!r} "
221
- ),
222
- }
223
- )
224
-
225
- raise nodes .SkipNode
226
-
227
-
228
- def main ():
42
+ def create_warn_unknown_block (
43
+ results : list [dict [str , t .Any ]], path : str
44
+ ) -> t .Callable [[int | str , int , str ], None ]:
45
+ def warn_unknown_block (line : int | str , col : int , content : str ) -> None :
46
+ results .append (
47
+ {
48
+ "path" : path ,
49
+ "line" : line ,
50
+ "col" : col ,
51
+ "message" : (
52
+ "Warning: found unknown literal block! Check for double colons '::'."
53
+ " If that is not the cause, please report this warning."
54
+ " It might indicate a bug in the checker or an unsupported Sphinx directive."
55
+ f" Content: { content !r} "
56
+ ),
57
+ }
58
+ )
59
+
60
+ return warn_unknown_block
61
+
62
+
63
+ def main () -> None :
229
64
paths = sys .argv [1 :] or sys .stdin .read ().splitlines ()
230
- results = []
231
-
232
- for directive in (
233
- "code" ,
234
- "code-block" ,
235
- "sourcecode" ,
236
- ):
237
- register_directive (directive , CodeBlockDirective )
238
-
239
- # The following docutils directives should better be ignored:
240
- for directive in ("parsed-literal" ,):
241
- register_directive (directive , IgnoreDirective )
65
+ results : list [dict [str , t .Any ]] = []
242
66
243
67
# TODO: should we handle the 'literalinclude' directive? maybe check file directly if right extension?
244
68
# (https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-literalinclude)
@@ -253,56 +77,84 @@ def main():
253
77
with open (path , "rt" , encoding = "utf-8" ) as f :
254
78
content = f .read ()
255
79
256
- # We create a Publisher only to have a mechanism which gives us the settings object.
257
- # Doing this more explicit is a bad idea since the classes used are deprecated and will
258
- # eventually get replaced. Publisher.get_settings() looks like a stable enough API that
259
- # we can 'just use'.
260
- publisher = Publisher (source_class = StringInput )
261
- publisher .set_components ("standalone" , "restructuredtext" , "pseudoxml" )
262
- override = {
263
- "root_prefix" : docs_root ,
264
- "input_encoding" : "utf-8" ,
265
- "file_insertion_enabled" : False ,
266
- "raw_enabled" : False ,
267
- "_disable_config" : True ,
268
- "report_level" : Reporter .ERROR_LEVEL ,
269
- "warning_stream" : io .StringIO (),
270
- }
271
- publisher .process_programmatic_settings (None , override , None )
272
- publisher .set_source (content , path )
273
-
274
- # Parse the document
275
80
try :
276
- doc = publisher .reader .read (
277
- publisher .source , publisher .parser , publisher .settings
278
- )
279
- except SystemMessage as exc :
280
- error = str (exc ).replace ("\n " , " / " )
281
- results .append (
282
- {
283
- "path" : path ,
284
- "line" : 0 ,
285
- "col" : 0 ,
286
- "message" : f"Cannot parse document: { error } " ,
287
- }
288
- )
289
- continue
290
- except Exception as exc :
291
- error = str (exc ).replace ("\n " , " / " )
292
- results .append (
293
- {
294
- "path" : path ,
295
- "line" : 0 ,
296
- "col" : 0 ,
297
- "message" : f"Cannot parse document, unexpected error { type (exc )} : { error } ; traceback: { traceback .format_exc ()!r} " ,
298
- }
299
- )
300
- continue
81
+ for code_block in find_code_blocks (
82
+ content ,
83
+ path = path ,
84
+ root_prefix = docs_root ,
85
+ warn_unknown_block = create_warn_unknown_block (results , path ),
86
+ ):
87
+ # Now that we have the offsets, we can actually do some processing...
88
+ if code_block .language not in {
89
+ "YAML" ,
90
+ "yaml" ,
91
+ "yaml+jinja" ,
92
+ "YAML+Jinja" ,
93
+ }:
94
+ if code_block .language is None :
95
+ allowed_languages = ", " .join (sorted (ALLOWED_LANGUAGES ))
96
+ results .append (
97
+ {
98
+ "path" : path ,
99
+ "line" : code_block .row_offset + 1 ,
100
+ "col" : code_block .col_offset + 1 ,
101
+ "message" : (
102
+ "Literal block without language!"
103
+ f" Allowed languages are: { allowed_languages } ."
104
+ ),
105
+ }
106
+ )
107
+ return
108
+ if code_block .language not in ALLOWED_LANGUAGES :
109
+ allowed_languages = ", " .join (sorted (ALLOWED_LANGUAGES ))
110
+ results .append (
111
+ {
112
+ "path" : path ,
113
+ "line" : code_block .row_offset + 1 ,
114
+ "col" : code_block .col_offset + 1 ,
115
+ "message" : (
116
+ f"Warning: literal block with disallowed language: { code_block .language } ."
117
+ " If the language should be allowed, the checker needs to be updated."
118
+ f" Currently allowed languages are: { allowed_languages } ."
119
+ ),
120
+ }
121
+ )
122
+ continue
301
123
302
- # Process the document
303
- try :
304
- visitor = YamlLintVisitor (doc , path , results , content , yamllint_config )
305
- doc .walk (visitor )
124
+ # So we have YAML. Let's lint it!
125
+ try :
126
+ problems = linter .run (
127
+ io .StringIO (code_block .content ),
128
+ yamllint_config ,
129
+ path ,
130
+ )
131
+ for problem in problems :
132
+ if problem .level not in REPORT_LEVELS :
133
+ continue
134
+ msg = f"{ problem .level } : { problem .desc } "
135
+ if problem .rule :
136
+ msg += f" ({ problem .rule } )"
137
+ results .append (
138
+ {
139
+ "path" : path ,
140
+ "line" : code_block .row_offset + problem .line ,
141
+ "col" : code_block .col_offset + problem .column ,
142
+ "message" : msg ,
143
+ }
144
+ )
145
+ except Exception as exc :
146
+ error = str (exc ).replace ("\n " , " / " )
147
+ results .append (
148
+ {
149
+ "path" : path ,
150
+ "line" : code_block .row_offset + 1 ,
151
+ "col" : code_block .col_offset + 1 ,
152
+ "message" : (
153
+ f"Internal error while linting YAML: exception { type (exc )} :"
154
+ f" { error } ; traceback: { traceback .format_exc ()!r} "
155
+ ),
156
+ }
157
+ )
306
158
except Exception as exc :
307
159
error = str (exc ).replace ("\n " , " / " )
308
160
results .append (
0 commit comments