Skip to content

Commit b4e6be4

Browse files
committed
feat(analyze): add CLI options for pythonpath, variable, and variablefile to analyze code command; collect errors for unimportable command line variable files
1 parent 8c25db8 commit b4e6be4

File tree

9 files changed

+243
-54
lines changed

9 files changed

+243
-54
lines changed

.vscode/launch.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,11 @@
6666
// "E:\\source\\uvtestprj\\tests\\first.robotrepl"
6767
"analyze",
6868
"code",
69-
"tests"
69+
// "tests"
70+
// "repl",
71+
// "-v",
72+
// "asd:asd"
73+
7074
]
7175
},
7276
{

packages/analyze/src/robotcode/analyze/cli.py

Lines changed: 99 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
from pathlib import Path
22
from textwrap import indent
3-
from typing import Set, Tuple
3+
from typing import List, Optional, Set, Tuple
44

55
import click
66

77
from robotcode.analyze.config import AnalyzeConfig
8-
from robotcode.core.lsp.types import DiagnosticSeverity
8+
from robotcode.core.lsp.types import Diagnostic, DiagnosticSeverity
99
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
1013
from robotcode.plugin import Application, pass_application
1114
from robotcode.robot.config.loader import (
1215
load_robot_config_from_path,
1316
)
1417
from robotcode.robot.config.utils import get_config_files
1518

1619
from .__version__ import __version__
17-
from .code_analyzer import CodeAnalyzer
20+
from .code_analyzer import CodeAnalyzer, DocumentDiagnosticReport, FolderDiagnosticReport
1821

1922

2023
@click.group(
@@ -44,6 +47,7 @@ def analyze(app: Application) -> None:
4447

4548
class Statistic:
4649
def __init__(self) -> None:
50+
self.folders: Set[WorkspaceFolder] = set()
4751
self.files: Set[TextDocument] = set()
4852
self.errors = 0
4953
self.warnings = 0
@@ -69,6 +73,7 @@ def __str__(self) -> str:
6973
"-f",
7074
"--filter",
7175
"filter",
76+
metavar="PATTERN",
7277
type=str,
7378
multiple=True,
7479
help="""\
@@ -86,25 +91,32 @@ def __str__(self) -> str:
8691
@click.option(
8792
"-V",
8893
"--variablefile",
89-
metavar="path",
94+
metavar="PATH",
9095
type=str,
9196
multiple=True,
9297
help="Python or YAML file file to read variables from. see `robot --variablefile` option.",
9398
)
9499
@click.option(
95100
"-P",
96101
"--pythonpath",
97-
metavar="path",
102+
metavar="PATH",
98103
type=str,
99104
multiple=True,
100-
help="Additional locations (directories, ZIPs, JARs) where to search test libraries"
105+
help="Additional locations where to search test libraries"
101106
" and other extensions when they are imported. see `robot --pythonpath` option.",
102107
)
103108
@click.argument(
104109
"paths", nargs=-1, type=click.Path(exists=True, dir_okay=True, file_okay=True, readable=True, path_type=Path)
105110
)
106111
@pass_application
107-
def code(app: Application, filter: Tuple[str], paths: Tuple[Path]) -> None:
112+
def code(
113+
app: Application,
114+
filter: Tuple[str],
115+
variable: Tuple[str, ...],
116+
variablefile: Tuple[str, ...],
117+
pythonpath: Tuple[str, ...],
118+
paths: Tuple[Path],
119+
) -> None:
108120
"""\
109121
Performs static code analysis to detect syntax errors, missing keywords or variables,
110122
missing arguments, and more on the given *PATHS*. *PATHS* can be files or directories.
@@ -132,35 +144,45 @@ def code(app: Application, filter: Tuple[str], paths: Tuple[Path]) -> None:
132144
*(app.config.profiles or []), verbose_callback=app.verbose, error_callback=app.error
133145
).evaluated_with_env()
134146

147+
if variable:
148+
if robot_profile.variables is None:
149+
robot_profile.variables = {}
150+
for v in variable:
151+
name, value = v.split(":", 1) if ":" in v else (v, "")
152+
robot_profile.variables.update({name: value})
153+
154+
if pythonpath:
155+
if robot_profile.python_path is None:
156+
robot_profile.python_path = []
157+
robot_profile.python_path.extend(pythonpath)
158+
159+
if variablefile:
160+
if robot_profile.variable_files is None:
161+
robot_profile.variable_files = []
162+
for vf in variablefile:
163+
robot_profile.variable_files.append(vf)
164+
135165
statistics = Statistic()
136166
for e in CodeAnalyzer(
137167
app=app,
138168
analysis_config=analyzer_config.to_workspace_analysis_config(),
139169
robot_profile=robot_profile,
140170
root_folder=root_folder,
141171
).run(paths=paths, filter=filter):
142-
statistics.files.add(e.document)
143-
144-
doc_path = e.document.uri.to_path().relative_to(root_folder) if root_folder else e.document.uri.to_path()
145-
if e.items:
146-
147-
for item in e.items:
148-
severity = item.severity if item.severity is not None else DiagnosticSeverity.ERROR
149-
150-
if severity == DiagnosticSeverity.ERROR:
151-
statistics.errors += 1
152-
elif severity == DiagnosticSeverity.WARNING:
153-
statistics.warnings += 1
154-
elif severity == DiagnosticSeverity.INFORMATION:
155-
statistics.infos += 1
156-
elif severity == DiagnosticSeverity.HINT:
157-
statistics.hints += 1
158-
159-
app.echo(
160-
f"{doc_path}:{item.range.start.line + 1}:{item.range.start.character + 1}: "
161-
+ click.style(f"[{severity.name[0]}] {item.code}", fg=SEVERITY_COLORS[severity])
162-
+ f": {indent(item.message, prefix=' ').strip()}",
163-
)
172+
if isinstance(e, FolderDiagnosticReport):
173+
statistics.folders.add(e.folder)
174+
175+
if e.items:
176+
_print_diagnostics(app, root_folder, statistics, e.items, e.folder.uri.to_path())
177+
178+
elif isinstance(e, DocumentDiagnosticReport):
179+
statistics.files.add(e.document)
180+
181+
doc_path = (
182+
e.document.uri.to_path().relative_to(root_folder) if root_folder else e.document.uri.to_path()
183+
)
184+
if e.items:
185+
_print_diagnostics(app, root_folder, statistics, e.items, doc_path)
164186

165187
statistics_str = str(statistics)
166188
if statistics.errors > 0:
@@ -172,3 +194,51 @@ def code(app: Application, filter: Tuple[str], paths: Tuple[Path]) -> None:
172194

173195
except (TypeError, ValueError) as e:
174196
raise click.ClickException(str(e)) from e
197+
198+
199+
def _print_diagnostics(
200+
app: Application,
201+
root_folder: Optional[Path],
202+
statistics: Statistic,
203+
diagnostics: List[Diagnostic],
204+
folder_path: Optional[Path],
205+
print_range: bool = True,
206+
) -> None:
207+
for item in diagnostics:
208+
severity = item.severity if item.severity is not None else DiagnosticSeverity.ERROR
209+
210+
if severity == DiagnosticSeverity.ERROR:
211+
statistics.errors += 1
212+
elif severity == DiagnosticSeverity.WARNING:
213+
statistics.warnings += 1
214+
elif severity == DiagnosticSeverity.INFORMATION:
215+
statistics.infos += 1
216+
elif severity == DiagnosticSeverity.HINT:
217+
statistics.hints += 1
218+
219+
app.echo(
220+
(
221+
(
222+
f"{folder_path}:"
223+
+ (f"{item.range.start.line + 1}:{item.range.start.character + 1}: " if print_range else " ")
224+
)
225+
if folder_path and folder_path != root_folder
226+
else " "
227+
)
228+
+ click.style(f"[{severity.name[0]}] {item.code}", fg=SEVERITY_COLORS[severity])
229+
+ f": {indent(item.message, prefix=' ').strip()}",
230+
)
231+
232+
if item.related_information:
233+
for related in item.related_information or []:
234+
related_path = try_get_relative_path(Uri(related.location.uri).to_path(), root_folder)
235+
236+
app.echo(
237+
f" {related_path}:"
238+
+ (
239+
f"{related.location.range.start.line + 1}:{related.location.range.start.character + 1}: "
240+
if print_range
241+
else " "
242+
)
243+
+ f"{indent(related.message, prefix=' ').strip()}",
244+
)

packages/analyze/src/robotcode/analyze/code_analyzer.py

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from dataclasses import dataclass
22
from pathlib import Path
3-
from typing import Iterable, List, Optional
3+
from typing import Iterable, List, Optional, Union
44

55
from robotcode.core.ignore_spec import IgnoreSpec
66
from robotcode.core.lsp.types import Diagnostic
@@ -22,6 +22,12 @@ class DocumentDiagnosticReport:
2222
items: List[Diagnostic]
2323

2424

25+
@dataclass
26+
class FolderDiagnosticReport:
27+
folder: WorkspaceFolder
28+
items: List[Diagnostic]
29+
30+
2531
class CodeAnalyzer(DiagnosticsContext):
2632
def __init__(
2733
self,
@@ -72,25 +78,40 @@ def workspace(self) -> Workspace:
7278
def diagnostics(self) -> DiagnosticHandlers:
7379
return self._dispatcher
7480

75-
def run(self, paths: Iterable[Path] = {}, filter: Iterable[str] = {}) -> Iterable[DocumentDiagnosticReport]:
81+
def run(
82+
self, paths: Iterable[Path] = {}, filter: Iterable[str] = {}
83+
) -> Iterable[Union[DocumentDiagnosticReport, FolderDiagnosticReport]]:
7684
for folder in self.workspace.workspace_folders:
85+
self.app.verbose(f"Initialize folder {folder.uri.to_path()}")
86+
initialize_result = self.diagnostics.initialize_folder(folder)
87+
if initialize_result is not None:
88+
diagnostics: List[Diagnostic] = []
89+
for item in initialize_result:
90+
if item is None:
91+
continue
92+
elif isinstance(item, BaseException):
93+
self.app.error(f"Error analyzing {folder.uri.to_path()}: {item}")
94+
else:
95+
diagnostics.extend(item)
96+
if diagnostics:
97+
yield FolderDiagnosticReport(folder, diagnostics)
7798

7899
documents = self.collect_documents(folder, paths=paths, filter=filter)
79100

80101
self.app.verbose(f"Analyzing {len(documents)} documents")
81102
for document in documents:
82103
analyze_result = self.diagnostics.analyze_document(document)
83104
if analyze_result is not None:
84-
diagnostics: List[Diagnostic] = []
105+
diagnostics = []
85106
for item in analyze_result:
86107
if item is None:
87108
continue
88109
elif isinstance(item, BaseException):
89-
self.app.error(f"Error analyzing {document.uri}: {item}")
110+
self.app.error(f"Error analyzing {document.uri.to_path()}: {item}")
90111
else:
91112
diagnostics.extend(item)
92-
93-
yield DocumentDiagnosticReport(document, diagnostics)
113+
if diagnostics:
114+
yield DocumentDiagnosticReport(document, diagnostics)
94115

95116
self.app.verbose(f"Collect Diagnostics for {len(documents)} documents")
96117
for document in documents:
@@ -101,11 +122,11 @@ def run(self, paths: Iterable[Path] = {}, filter: Iterable[str] = {}) -> Iterabl
101122
if item is None:
102123
continue
103124
elif isinstance(item, BaseException):
104-
self.app.error(f"Error analyzing {document.uri}: {item}")
125+
self.app.error(f"Error analyzing {document.uri.to_path()}: {item}")
105126
else:
106127
diagnostics.extend(item)
107-
108-
yield DocumentDiagnosticReport(document, diagnostics)
128+
if diagnostics:
129+
yield DocumentDiagnosticReport(document, diagnostics)
109130

110131
def collect_documents(
111132
self, folder: WorkspaceFolder, paths: Iterable[Path] = {}, filter: Iterable[str] = {}

packages/analyze/src/robotcode/analyze/diagnostics_context.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,29 @@
55
from robotcode.core.language import language_id_filter
66
from robotcode.core.lsp.types import Diagnostic
77
from robotcode.core.text_document import TextDocument
8-
from robotcode.core.workspace import Workspace
8+
from robotcode.core.workspace import Workspace, WorkspaceFolder
99
from robotcode.robot.config.model import RobotBaseProfile
1010
from robotcode.robot.diagnostics.workspace_config import WorkspaceAnalysisConfig
1111

1212

1313
class DiagnosticHandlers:
1414
@event
15-
def analyzers(sender, document: TextDocument) -> Optional[List[Diagnostic]]: ...
15+
def document_analyzers(sender, document: TextDocument) -> Optional[List[Diagnostic]]: ...
16+
@event
17+
def folder_initializers(sender, folder: WorkspaceFolder) -> Optional[List[Diagnostic]]: ...
1618

1719
@event
1820
def collectors(sender, document: TextDocument) -> Optional[List[Diagnostic]]: ...
1921

22+
def initialize_folder(self, folder: WorkspaceFolder) -> List[Union[List[Diagnostic], BaseException, None]]:
23+
return self.folder_initializers(
24+
self,
25+
folder,
26+
return_exceptions=True,
27+
)
28+
2029
def analyze_document(self, document: TextDocument) -> List[Union[List[Diagnostic], BaseException, None]]:
21-
return self.analyzers(
30+
return self.document_analyzers(
2231
self,
2332
document,
2433
callback_filter=language_id_filter(document),

packages/analyze/src/robotcode/analyze/robot_framework_language_provider.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import glob
12
import sys
23
from pathlib import Path
34
from typing import Any, Iterable, List, Optional
@@ -46,7 +47,8 @@ def __init__(self, diagnostics_context: DiagnosticsContext) -> None:
4647
)
4748

4849
self.diagnostics_context.workspace.documents.on_read_document_text.add(self.on_read_document_text)
49-
self.diagnostics_context.diagnostics.analyzers.add(self.analyze_document)
50+
self.diagnostics_context.diagnostics.folder_initializers.add(self.analyze_folder)
51+
self.diagnostics_context.diagnostics.document_analyzers.add(self.analyze_document)
5052

5153
def _update_python_path(self) -> None:
5254
if self.diagnostics_context.workspace.root_uri is not None:
@@ -56,8 +58,9 @@ def _update_python_path(self) -> None:
5658
pa = Path(self.diagnostics_context.workspace.root_uri.to_path(), pa)
5759

5860
absolute_path = str(pa.absolute())
59-
if absolute_path not in sys.path:
60-
sys.path.insert(0, absolute_path)
61+
for f in glob.glob(absolute_path):
62+
if Path(f).is_dir() and f not in sys.path:
63+
sys.path.insert(0, f)
6164

6265
@language_id("robotframework")
6366
def on_read_document_text(self, sender: Any, uri: Uri) -> str:
@@ -95,3 +98,8 @@ def analyze_document(self, sender: Any, document: TextDocument) -> Optional[List
9598
namespace.analyze()
9699

97100
return self._document_cache.get_diagnostic_modifier(document).modify_diagnostics(namespace.get_diagnostics())
101+
102+
def analyze_folder(self, sender: Any, folder: WorkspaceFolder) -> Optional[List[Diagnostic]]:
103+
imports_manager = self._document_cache.get_imports_manager_for_workspace_folder(folder)
104+
105+
return imports_manager.diagnostics

packages/core/src/robotcode/core/lsp/types.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3988,6 +3988,18 @@ def is_in_range(self, range: Range, include_end: bool = True) -> bool:
39883988
def __hash__(self) -> int:
39893989
return hash((self.start, self.end))
39903990

3991+
@staticmethod
3992+
def from_int_range(
3993+
start_line: int, start_character: int = 0, end_line: Optional[int] = None, end_character: Optional[int] = None
3994+
) -> Range:
3995+
return Range(
3996+
start=Position(line=start_line, character=start_character),
3997+
end=Position(
3998+
line=end_line if end_line is not None else start_line,
3999+
character=end_character if end_character is not None else start_character,
4000+
),
4001+
)
4002+
39914003

39924004
@dataclass
39934005
class WorkspaceFoldersChangeEvent(CamelSnakeMixin):

0 commit comments

Comments
 (0)