Skip to content

Commit 0123a50

Browse files
committed
refactor(analyze): move code analysing to it's own module
1 parent 161006e commit 0123a50

File tree

6 files changed

+379
-366
lines changed

6 files changed

+379
-366
lines changed
+2-354
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,9 @@
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-
61
import 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
133
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
184

195
from .__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

Comments
 (0)