1
- from enum import Flag
2
- from pathlib import Path
3
- from textwrap import indent
4
- from typing import List , Optional , Set , Tuple , Union
5
-
6
1
import click
7
2
8
- from robotcode .core .lsp .types import Diagnostic , DiagnosticSeverity
9
- from robotcode .core .text_document import TextDocument
10
- from robotcode .core .uri import Uri
11
- from robotcode .core .utils .path import try_get_relative_path
12
- from robotcode .core .workspace import WorkspaceFolder
13
3
from robotcode .plugin import Application , pass_application
14
- from robotcode .robot .config .loader import (
15
- load_robot_config_from_path ,
16
- )
17
- from robotcode .robot .config .utils import get_config_files
18
4
19
5
from .__version__ import __version__
20
- from .code_analyzer import CodeAnalyzer , DocumentDiagnosticReport , FolderDiagnosticReport
21
- from .config import AnalyzeConfig , ModifiersConfig
6
+ from .code .cli import code
22
7
23
8
24
9
@click .group (
@@ -38,341 +23,4 @@ def analyze(app: Application) -> None:
38
23
"""
39
24
40
25
41
- SEVERITY_COLORS = {
42
- DiagnosticSeverity .ERROR : "red" ,
43
- DiagnosticSeverity .WARNING : "yellow" ,
44
- DiagnosticSeverity .INFORMATION : "blue" ,
45
- DiagnosticSeverity .HINT : "cyan" ,
46
- }
47
-
48
-
49
- class ReturnCode (Flag ):
50
- SUCCESS = 0
51
- ERRORS = 1
52
- WARNINGS = 2
53
- INFOS = 4
54
- HINTS = 8
55
-
56
-
57
- class Statistic :
58
- def __init__ (self ) -> None :
59
- self ._folders : Set [WorkspaceFolder ] = set ()
60
- self ._files : Set [TextDocument ] = set ()
61
- self ._diagnostics : List [Union [DocumentDiagnosticReport , FolderDiagnosticReport ]] = []
62
-
63
- @property
64
- def errors (self ) -> int :
65
- return sum (
66
- len ([i for i in e .items if i .severity == DiagnosticSeverity .ERROR ]) for e in self ._diagnostics if e .items
67
- )
68
-
69
- @property
70
- def warnings (self ) -> int :
71
- return sum (
72
- len ([i for i in e .items if i .severity == DiagnosticSeverity .WARNING ]) for e in self ._diagnostics if e .items
73
- )
74
-
75
- @property
76
- def infos (self ) -> int :
77
- return sum (
78
- len ([i for i in e .items if i .severity == DiagnosticSeverity .INFORMATION ])
79
- for e in self ._diagnostics
80
- if e .items
81
- )
82
-
83
- @property
84
- def hints (self ) -> int :
85
- return sum (
86
- len ([i for i in e .items if i .severity == DiagnosticSeverity .HINT ]) for e in self ._diagnostics if e .items
87
- )
88
-
89
- def add_diagnostics_report (
90
- self , diagnostics_report : Union [DocumentDiagnosticReport , FolderDiagnosticReport ]
91
- ) -> None :
92
- self ._diagnostics .append (diagnostics_report )
93
-
94
- if isinstance (diagnostics_report , FolderDiagnosticReport ):
95
- self ._folders .add (diagnostics_report .folder )
96
- elif isinstance (diagnostics_report , DocumentDiagnosticReport ):
97
- self ._files .add (diagnostics_report .document )
98
-
99
- def __str__ (self ) -> str :
100
- return (
101
- f"Files: { len (self ._files )} , Errors: { self .errors } , Warnings: { self .warnings } , "
102
- f"Infos: { self .infos } , Hints: { self .hints } "
103
- )
104
-
105
- def calculate_return_code (self ) -> ReturnCode :
106
- return_code = ReturnCode .SUCCESS
107
- if self .errors > 0 :
108
- return_code |= ReturnCode .ERRORS
109
- if self .warnings > 0 :
110
- return_code |= ReturnCode .WARNINGS
111
- if self .infos > 0 :
112
- return_code |= ReturnCode .INFOS
113
- if self .hints > 0 :
114
- return_code |= ReturnCode .HINTS
115
- return return_code
116
-
117
-
118
- @analyze .command (
119
- add_help_option = True ,
120
- )
121
- @click .version_option (
122
- version = __version__ ,
123
- package_name = "robotcode.analyze" ,
124
- prog_name = "RobotCode Analyze" ,
125
- )
126
- @click .option (
127
- "-f" ,
128
- "--filter" ,
129
- "filter" ,
130
- metavar = "PATTERN" ,
131
- type = str ,
132
- multiple = True ,
133
- help = """\
134
- Glob pattern to filter files to analyze. Can be specified multiple times.
135
- """ ,
136
- )
137
- @click .option (
138
- "-v" ,
139
- "--variable" ,
140
- metavar = "name:value" ,
141
- type = str ,
142
- multiple = True ,
143
- help = "Set variables in the test data. see `robot --variable` option." ,
144
- )
145
- @click .option (
146
- "-V" ,
147
- "--variablefile" ,
148
- metavar = "PATH" ,
149
- type = str ,
150
- multiple = True ,
151
- help = "Python or YAML file file to read variables from. see `robot --variablefile` option." ,
152
- )
153
- @click .option (
154
- "-P" ,
155
- "--pythonpath" ,
156
- metavar = "PATH" ,
157
- type = str ,
158
- multiple = True ,
159
- help = "Additional locations where to search test libraries"
160
- " and other extensions when they are imported. see `robot --pythonpath` option." ,
161
- )
162
- @click .option (
163
- "-mi" ,
164
- "--modifiers-ignore" ,
165
- metavar = "CODE" ,
166
- type = str ,
167
- multiple = True ,
168
- help = "Specifies the diagnostics codes to ignore." ,
169
- )
170
- @click .option (
171
- "-me" ,
172
- "--modifiers-error" ,
173
- metavar = "CODE" ,
174
- type = str ,
175
- multiple = True ,
176
- help = "Specifies the diagnostics codes to treat as errors." ,
177
- )
178
- @click .option (
179
- "-mw" ,
180
- "--modifiers-warning" ,
181
- metavar = "CODE" ,
182
- type = str ,
183
- multiple = True ,
184
- help = "Specifies the diagnostics codes to treat as warning." ,
185
- )
186
- @click .option (
187
- "-mI" ,
188
- "--modifiers-information" ,
189
- metavar = "CODE" ,
190
- type = str ,
191
- multiple = True ,
192
- help = "Specifies the diagnostics codes to treat as information." ,
193
- )
194
- @click .option (
195
- "-mh" ,
196
- "--modifiers-hint" ,
197
- metavar = "CODE" ,
198
- type = str ,
199
- multiple = True ,
200
- help = "Specifies the diagnostics codes to treat as hint." ,
201
- )
202
- @click .argument (
203
- "paths" , nargs = - 1 , type = click .Path (exists = True , dir_okay = True , file_okay = True , readable = True , path_type = Path )
204
- )
205
- @pass_application
206
- def code (
207
- app : Application ,
208
- filter : Tuple [str , ...],
209
- variable : Tuple [str , ...],
210
- variablefile : Tuple [str , ...],
211
- pythonpath : Tuple [str , ...],
212
- modifiers_ignore : Tuple [str , ...],
213
- modifiers_error : Tuple [str , ...],
214
- modifiers_warning : Tuple [str , ...],
215
- modifiers_information : Tuple [str , ...],
216
- modifiers_hint : Tuple [str , ...],
217
- paths : Tuple [Path ],
218
- ) -> None :
219
- """\
220
- Performs static code analysis to identify potential issues in the specified *PATHS*. The analysis detects syntax
221
- errors, missing keywords or variables, missing arguments, and other problems.
222
-
223
- - **PATHS**: Can be individual files or directories. If no *PATHS* are provided, the current directory is
224
- analyzed by default.
225
-
226
- The return code is a bitwise combination of the following values:
227
-
228
- - `0`: **SUCCESS** - No issues detected.
229
- - `1`: **ERRORS** - Critical issues found.
230
- - `2`: **WARNINGS** - Non-critical issues detected.
231
- - `4`: **INFORMATIONS** - General information messages.
232
- - `8`: **HINTS** - Suggestions or improvements.
233
-
234
- \b
235
- *Examples*:
236
- ```
237
- robotcode analyze code
238
- robotcode analyze code --filter **/*.robot
239
- robotcode analyze code tests/acceptance/first.robot
240
- robotcode analyze code -mi DuplicateKeyword tests/acceptance/first.robot
241
- robotcode --format json analyze code
242
- ```
243
- """
244
-
245
- config_files , root_folder , _ = get_config_files (
246
- paths ,
247
- app .config .config_files ,
248
- root_folder = app .config .root ,
249
- no_vcs = app .config .no_vcs ,
250
- verbose_callback = app .verbose ,
251
- )
252
-
253
- try :
254
- robot_config = load_robot_config_from_path (
255
- * config_files , extra_tools = {"robotcode-analyze" : AnalyzeConfig }, verbose_callback = app .verbose
256
- )
257
-
258
- analyzer_config = robot_config .tool .get ("robotcode-analyze" , None ) if robot_config .tool is not None else None
259
- if analyzer_config is None :
260
- analyzer_config = AnalyzeConfig ()
261
-
262
- robot_profile = robot_config .combine_profiles (
263
- * (app .config .profiles or []), verbose_callback = app .verbose , error_callback = app .error
264
- ).evaluated_with_env ()
265
-
266
- if variable :
267
- if robot_profile .variables is None :
268
- robot_profile .variables = {}
269
- for v in variable :
270
- name , value = v .split (":" , 1 ) if ":" in v else (v , "" )
271
- robot_profile .variables .update ({name : value })
272
-
273
- if pythonpath :
274
- if robot_profile .python_path is None :
275
- robot_profile .python_path = []
276
- robot_profile .python_path .extend (pythonpath )
277
-
278
- if variablefile :
279
- if robot_profile .variable_files is None :
280
- robot_profile .variable_files = []
281
- for vf in variablefile :
282
- robot_profile .variable_files .append (vf )
283
-
284
- if analyzer_config .modifiers is None :
285
- analyzer_config .modifiers = ModifiersConfig ()
286
-
287
- if modifiers_ignore :
288
- if analyzer_config .modifiers .ignore is None :
289
- analyzer_config .modifiers .ignore = []
290
- analyzer_config .modifiers .ignore .extend (modifiers_ignore )
291
-
292
- if modifiers_error :
293
- if analyzer_config .modifiers .error is None :
294
- analyzer_config .modifiers .error = []
295
- analyzer_config .modifiers .error .extend (modifiers_error )
296
-
297
- if modifiers_warning :
298
- if analyzer_config .modifiers .warning is None :
299
- analyzer_config .modifiers .warning = []
300
- analyzer_config .modifiers .warning .extend (modifiers_warning )
301
-
302
- if modifiers_information :
303
- if analyzer_config .modifiers .information is None :
304
- analyzer_config .modifiers .information = []
305
- analyzer_config .modifiers .information .extend (modifiers_information )
306
-
307
- if modifiers_hint :
308
- if analyzer_config .modifiers .hint is None :
309
- analyzer_config .modifiers .hint = []
310
- analyzer_config .modifiers .hint .extend (modifiers_hint )
311
-
312
- statistics = Statistic ()
313
- for e in CodeAnalyzer (
314
- app = app ,
315
- analysis_config = analyzer_config .to_workspace_analysis_config (),
316
- robot_profile = robot_profile ,
317
- root_folder = root_folder ,
318
- ).run (paths = paths , filter = filter ):
319
- statistics .add_diagnostics_report (e )
320
-
321
- if isinstance (e , FolderDiagnosticReport ):
322
- if e .items :
323
- _print_diagnostics (app , root_folder , e .items , e .folder .uri .to_path ())
324
- elif isinstance (e , DocumentDiagnosticReport ):
325
- doc_path = (
326
- e .document .uri .to_path ().relative_to (root_folder ) if root_folder else e .document .uri .to_path ()
327
- )
328
- if e .items :
329
- _print_diagnostics (app , root_folder , e .items , doc_path )
330
-
331
- statistics_str = str (statistics )
332
- if statistics .errors > 0 :
333
- statistics_str = click .style (statistics_str , fg = "red" )
334
-
335
- app .echo (statistics_str )
336
-
337
- app .exit (statistics .calculate_return_code ().value )
338
-
339
- except (TypeError , ValueError ) as e :
340
- raise click .ClickException (str (e )) from e
341
-
342
-
343
- def _print_diagnostics (
344
- app : Application ,
345
- root_folder : Optional [Path ],
346
- diagnostics : List [Diagnostic ],
347
- folder_path : Optional [Path ],
348
- print_range : bool = True ,
349
- ) -> None :
350
- for item in diagnostics :
351
- severity = item .severity if item .severity is not None else DiagnosticSeverity .ERROR
352
-
353
- app .echo (
354
- (
355
- (
356
- f"{ folder_path } :"
357
- + (f"{ item .range .start .line + 1 } :{ item .range .start .character + 1 } : " if print_range else " " )
358
- )
359
- if folder_path and folder_path != root_folder
360
- else ""
361
- )
362
- + click .style (f"[{ severity .name [0 ]} ] { item .code } " , fg = SEVERITY_COLORS [severity ])
363
- + f": { indent (item .message , prefix = ' ' ).strip ()} " ,
364
- )
365
-
366
- if item .related_information :
367
- for related in item .related_information or []:
368
- related_path = try_get_relative_path (Uri (related .location .uri ).to_path (), root_folder )
369
-
370
- app .echo (
371
- f" { related_path } :"
372
- + (
373
- f"{ related .location .range .start .line + 1 } :{ related .location .range .start .character + 1 } : "
374
- if print_range
375
- else " "
376
- )
377
- + f"{ indent (related .message , prefix = ' ' ).strip ()} " ,
378
- )
26
+ analyze .add_command (code )
0 commit comments