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-
61import click
72
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
133from 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
184
195from .__version__ import __version__
20- from .code_analyzer import CodeAnalyzer , DocumentDiagnosticReport , FolderDiagnosticReport
21- from .config import AnalyzeConfig , ModifiersConfig
6+ from .code .cli import code
227
238
249@click .group (
@@ -38,341 +23,4 @@ def analyze(app: Application) -> None:
3823 """
3924
4025
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