robotcode-analyze 0.96.0__tar.gz → 0.97.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: robotcode-analyze
3
- Version: 0.96.0
3
+ Version: 0.97.0
4
4
  Summary: RobotCode analyze plugin for Robot Framework
5
5
  Project-URL: Homepage, https://robotcode.io
6
6
  Project-URL: Donate, https://opencollective.com/robotcode
@@ -10,7 +10,6 @@ Project-URL: Issues, https://github.com/robotcodedev/robotcode/issues
10
10
  Project-URL: Source, https://github.com/robotcodedev/robotcode
11
11
  Author-email: Daniel Biehl <dbiehl@live.de>
12
12
  License: Apache-2.0
13
- License-File: LICENSE.txt
14
13
  Classifier: Development Status :: 5 - Production/Stable
15
14
  Classifier: Framework :: Robot Framework
16
15
  Classifier: Framework :: Robot Framework :: Tool
@@ -25,9 +24,9 @@ Classifier: Programming Language :: Python :: Implementation :: PyPy
25
24
  Classifier: Topic :: Utilities
26
25
  Classifier: Typing :: Typed
27
26
  Requires-Python: >=3.8
28
- Requires-Dist: robotcode-plugin==0.96.0
29
- Requires-Dist: robotcode-robot==0.96.0
30
- Requires-Dist: robotcode==0.96.0
27
+ Requires-Dist: robotcode-plugin==0.97.0
28
+ Requires-Dist: robotcode-robot==0.97.0
29
+ Requires-Dist: robotcode==0.97.0
31
30
  Requires-Dist: robotframework>=4.1.0
32
31
  Description-Content-Type: text/markdown
33
32
 
@@ -27,9 +27,9 @@ classifiers = [
27
27
  ]
28
28
  dependencies = [
29
29
  "robotframework>=4.1.0",
30
- "robotcode-plugin==0.96.0",
31
- "robotcode-robot==0.96.0",
32
- "robotcode==0.96.0",
30
+ "robotcode-plugin==0.97.0",
31
+ "robotcode-robot==0.97.0",
32
+ "robotcode==0.97.0",
33
33
  ]
34
34
  dynamic = ["version"]
35
35
 
@@ -0,0 +1 @@
1
+ __version__ = "0.97.0"
@@ -0,0 +1,244 @@
1
+ from pathlib import Path
2
+ from textwrap import indent
3
+ from typing import List, Optional, Set, Tuple
4
+
5
+ import click
6
+
7
+ from robotcode.analyze.config import AnalyzeConfig
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
+ 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
+
19
+ from .__version__ import __version__
20
+ from .code_analyzer import CodeAnalyzer, DocumentDiagnosticReport, FolderDiagnosticReport
21
+
22
+
23
+ @click.group(
24
+ add_help_option=True,
25
+ invoke_without_command=False,
26
+ )
27
+ @click.version_option(
28
+ version=__version__,
29
+ package_name="robotcode.analyze",
30
+ prog_name="RobotCode Analyze",
31
+ )
32
+ @pass_application
33
+ def analyze(app: Application) -> None:
34
+ """\
35
+ The analyze command provides various subcommands for analyzing Robot Framework code.
36
+ These subcommands support specialized tasks, such as code analysis, style checking or dependency graphs.
37
+ """
38
+
39
+
40
+ SEVERITY_COLORS = {
41
+ DiagnosticSeverity.ERROR: "red",
42
+ DiagnosticSeverity.WARNING: "yellow",
43
+ DiagnosticSeverity.INFORMATION: "blue",
44
+ DiagnosticSeverity.HINT: "cyan",
45
+ }
46
+
47
+
48
+ class Statistic:
49
+ def __init__(self) -> None:
50
+ self.folders: Set[WorkspaceFolder] = set()
51
+ self.files: Set[TextDocument] = set()
52
+ self.errors = 0
53
+ self.warnings = 0
54
+ self.infos = 0
55
+ self.hints = 0
56
+
57
+ def __str__(self) -> str:
58
+ return (
59
+ f"Files: {len(self.files)}, Errors: {self.errors}, Warnings: {self.warnings}, "
60
+ f"Infos: {self.infos}, Hints: {self.hints}"
61
+ )
62
+
63
+
64
+ @analyze.command(
65
+ add_help_option=True,
66
+ )
67
+ @click.version_option(
68
+ version=__version__,
69
+ package_name="robotcode.analyze",
70
+ prog_name="RobotCode Analyze",
71
+ )
72
+ @click.option(
73
+ "-f",
74
+ "--filter",
75
+ "filter",
76
+ metavar="PATTERN",
77
+ type=str,
78
+ multiple=True,
79
+ help="""\
80
+ Glob pattern to filter files to analyze. Can be specified multiple times.
81
+ """,
82
+ )
83
+ @click.option(
84
+ "-v",
85
+ "--variable",
86
+ metavar="name:value",
87
+ type=str,
88
+ multiple=True,
89
+ help="Set variables in the test data. see `robot --variable` option.",
90
+ )
91
+ @click.option(
92
+ "-V",
93
+ "--variablefile",
94
+ metavar="PATH",
95
+ type=str,
96
+ multiple=True,
97
+ help="Python or YAML file file to read variables from. see `robot --variablefile` option.",
98
+ )
99
+ @click.option(
100
+ "-P",
101
+ "--pythonpath",
102
+ metavar="PATH",
103
+ type=str,
104
+ multiple=True,
105
+ help="Additional locations where to search test libraries"
106
+ " and other extensions when they are imported. see `robot --pythonpath` option.",
107
+ )
108
+ @click.argument(
109
+ "paths", nargs=-1, type=click.Path(exists=True, dir_okay=True, file_okay=True, readable=True, path_type=Path)
110
+ )
111
+ @pass_application
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:
120
+ """\
121
+ Performs static code analysis to detect syntax errors, missing keywords or variables,
122
+ missing arguments, and more on the given *PATHS*. *PATHS* can be files or directories.
123
+ If no PATHS are given, the current directory is used.
124
+ """
125
+
126
+ config_files, root_folder, _ = get_config_files(
127
+ paths,
128
+ app.config.config_files,
129
+ root_folder=app.config.root,
130
+ no_vcs=app.config.no_vcs,
131
+ verbose_callback=app.verbose,
132
+ )
133
+
134
+ try:
135
+ robot_config = load_robot_config_from_path(
136
+ *config_files, extra_tools={"robotcode-analyze": AnalyzeConfig}, verbose_callback=app.verbose
137
+ )
138
+
139
+ analyzer_config = robot_config.tool.get("robotcode-analyze", None) if robot_config.tool is not None else None
140
+ if analyzer_config is None:
141
+ analyzer_config = AnalyzeConfig()
142
+
143
+ robot_profile = robot_config.combine_profiles(
144
+ *(app.config.profiles or []), verbose_callback=app.verbose, error_callback=app.error
145
+ ).evaluated_with_env()
146
+
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
+
165
+ statistics = Statistic()
166
+ for e in CodeAnalyzer(
167
+ app=app,
168
+ analysis_config=analyzer_config.to_workspace_analysis_config(),
169
+ robot_profile=robot_profile,
170
+ root_folder=root_folder,
171
+ ).run(paths=paths, filter=filter):
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)
186
+
187
+ statistics_str = str(statistics)
188
+ if statistics.errors > 0:
189
+ statistics_str = click.style(statistics_str, fg="red")
190
+
191
+ app.echo(statistics_str)
192
+
193
+ app.exit(statistics.errors)
194
+
195
+ except (TypeError, ValueError) as e:
196
+ 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
+ )
@@ -1,6 +1,6 @@
1
1
  from dataclasses import dataclass
2
2
  from pathlib import Path
3
- from typing import Iterable, List, Optional
3
+ from typing import Iterable, List, Optional, Union
4
4
 
5
5
  from robotcode.core.ignore_spec import IgnoreSpec
6
6
  from robotcode.core.lsp.types import Diagnostic
@@ -12,7 +12,6 @@ from robotcode.plugin import Application
12
12
  from robotcode.robot.config.model import RobotBaseProfile
13
13
  from robotcode.robot.diagnostics.workspace_config import WorkspaceAnalysisConfig
14
14
 
15
- from .config import AnalyzeConfig
16
15
  from .diagnostics_context import DiagnosticHandlers, DiagnosticsContext
17
16
  from .robot_framework_language_provider import RobotFrameworkLanguageProvider
18
17
 
@@ -23,16 +22,23 @@ class DocumentDiagnosticReport:
23
22
  items: List[Diagnostic]
24
23
 
25
24
 
25
+ @dataclass
26
+ class FolderDiagnosticReport:
27
+ folder: WorkspaceFolder
28
+ items: List[Diagnostic]
29
+
30
+
26
31
  class CodeAnalyzer(DiagnosticsContext):
27
32
  def __init__(
28
33
  self,
29
34
  app: Application,
30
- config: AnalyzeConfig,
35
+ analysis_config: WorkspaceAnalysisConfig,
31
36
  robot_profile: RobotBaseProfile,
32
37
  root_folder: Optional[Path],
33
38
  ):
34
39
  self.app = app
35
- self._config = config
40
+ self._analysis_config = analysis_config or WorkspaceAnalysisConfig()
41
+
36
42
  self._robot_profile = robot_profile
37
43
  self._root_folder = root_folder if root_folder is not None else Path.cwd()
38
44
 
@@ -53,8 +59,8 @@ class CodeAnalyzer(DiagnosticsContext):
53
59
  handler.verbose_callback = app.verbose
54
60
 
55
61
  @property
56
- def analysis_config(self) -> Optional[WorkspaceAnalysisConfig]:
57
- return None
62
+ def analysis_config(self) -> WorkspaceAnalysisConfig:
63
+ return self._analysis_config
58
64
 
59
65
  @property
60
66
  def profile(self) -> RobotBaseProfile:
@@ -72,8 +78,23 @@ class CodeAnalyzer(DiagnosticsContext):
72
78
  def diagnostics(self) -> DiagnosticHandlers:
73
79
  return self._dispatcher
74
80
 
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]]:
76
84
  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)
77
98
 
78
99
  documents = self.collect_documents(folder, paths=paths, filter=filter)
79
100
 
@@ -81,16 +102,16 @@ class CodeAnalyzer(DiagnosticsContext):
81
102
  for document in documents:
82
103
  analyze_result = self.diagnostics.analyze_document(document)
83
104
  if analyze_result is not None:
84
- diagnostics: List[Diagnostic] = []
105
+ diagnostics = []
85
106
  for item in analyze_result:
86
107
  if item is None:
87
108
  continue
88
109
  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}")
90
111
  else:
91
112
  diagnostics.extend(item)
92
-
93
- yield DocumentDiagnosticReport(document, diagnostics)
113
+ if diagnostics:
114
+ yield DocumentDiagnosticReport(document, diagnostics)
94
115
 
95
116
  self.app.verbose(f"Collect Diagnostics for {len(documents)} documents")
96
117
  for document in documents:
@@ -101,11 +122,11 @@ class CodeAnalyzer(DiagnosticsContext):
101
122
  if item is None:
102
123
  continue
103
124
  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}")
105
126
  else:
106
127
  diagnostics.extend(item)
107
-
108
- yield DocumentDiagnosticReport(document, diagnostics)
128
+ if diagnostics:
129
+ yield DocumentDiagnosticReport(document, diagnostics)
109
130
 
110
131
  def collect_documents(
111
132
  self, folder: WorkspaceFolder, paths: Iterable[Path] = {}, filter: Iterable[str] = {}
@@ -3,6 +3,12 @@ from dataclasses import dataclass
3
3
  from typing import List, Optional
4
4
 
5
5
  from robotcode.robot.config.model import BaseOptions, field
6
+ from robotcode.robot.diagnostics.workspace_config import (
7
+ AnalysisDiagnosticModifiersConfig,
8
+ AnalysisRobotConfig,
9
+ WorkspaceAnalysisConfig,
10
+ )
11
+ from robotcode.robot.diagnostics.workspace_config import CacheConfig as WorkspaceCacheConfig
6
12
 
7
13
 
8
14
  @dataclass
@@ -246,3 +252,30 @@ class AnalyzeConfig(BaseOptions):
246
252
  extend_global_library_search_order: Optional[List[str]] = field(
247
253
  description="Extend the global library search order setting."
248
254
  )
255
+
256
+ def to_workspace_analysis_config(self) -> WorkspaceAnalysisConfig:
257
+ return WorkspaceAnalysisConfig(
258
+ exclude_patterns=self.exclude_patterns or [],
259
+ cache=(
260
+ WorkspaceCacheConfig(
261
+ # TODO savelocation
262
+ ignored_libraries=self.cache.ignored_libraries or [],
263
+ ignored_variables=self.cache.ignored_variables or [],
264
+ ignore_arguments_for_library=self.cache.ignore_arguments_for_library or [],
265
+ )
266
+ if self.cache is not None
267
+ else WorkspaceCacheConfig()
268
+ ),
269
+ robot=AnalysisRobotConfig(global_library_search_order=self.global_library_search_order or []),
270
+ modifiers=(
271
+ AnalysisDiagnosticModifiersConfig(
272
+ ignore=self.modifiers.ignore or [],
273
+ error=self.modifiers.error or [],
274
+ warning=self.modifiers.warning or [],
275
+ information=self.modifiers.information or [],
276
+ hint=self.modifiers.hint or [],
277
+ )
278
+ if self.modifiers is not None
279
+ else AnalysisDiagnosticModifiersConfig()
280
+ ),
281
+ )
@@ -5,20 +5,29 @@ from robotcode.core.event import event
5
5
  from robotcode.core.language import language_id_filter
6
6
  from robotcode.core.lsp.types import Diagnostic
7
7
  from robotcode.core.text_document import TextDocument
8
- from robotcode.core.workspace import Workspace
8
+ from robotcode.core.workspace import Workspace, WorkspaceFolder
9
9
  from robotcode.robot.config.model import RobotBaseProfile
10
10
  from robotcode.robot.diagnostics.workspace_config import WorkspaceAnalysisConfig
11
11
 
12
12
 
13
13
  class DiagnosticHandlers:
14
14
  @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]]: ...
16
18
 
17
19
  @event
18
20
  def collectors(sender, document: TextDocument) -> Optional[List[Diagnostic]]: ...
19
21
 
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
+
20
29
  def analyze_document(self, document: TextDocument) -> List[Union[List[Diagnostic], BaseException, None]]:
21
- return self.analyzers(
30
+ return self.document_analyzers(
22
31
  self,
23
32
  document,
24
33
  callback_filter=language_id_filter(document),
@@ -37,7 +46,7 @@ class DiagnosticHandlers:
37
46
  class DiagnosticsContext(ABC):
38
47
  @property
39
48
  @abstractmethod
40
- def analysis_config(self) -> Optional[WorkspaceAnalysisConfig]: ...
49
+ def analysis_config(self) -> WorkspaceAnalysisConfig: ...
41
50
 
42
51
  @property
43
52
  @abstractmethod
@@ -1,3 +1,4 @@
1
+ import glob
1
2
  import sys
2
3
  from pathlib import Path
3
4
  from typing import Any, Iterable, List, Optional
@@ -46,7 +47,8 @@ class RobotFrameworkLanguageProvider(LanguageProvider):
46
47
  )
47
48
 
48
49
  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)
50
52
 
51
53
  def _update_python_path(self) -> None:
52
54
  if self.diagnostics_context.workspace.root_uri is not None:
@@ -56,8 +58,9 @@ class RobotFrameworkLanguageProvider(LanguageProvider):
56
58
  pa = Path(self.diagnostics_context.workspace.root_uri.to_path(), pa)
57
59
 
58
60
  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)
61
64
 
62
65
  @language_id("robotframework")
63
66
  def on_read_document_text(self, sender: Any, uri: Uri) -> str:
@@ -70,6 +73,10 @@ class RobotFrameworkLanguageProvider(LanguageProvider):
70
73
 
71
74
  extensions = self.LANGUAGE_DEFINITION.extensions
72
75
 
76
+ exclude_patterns = [
77
+ *self.diagnostics_context.analysis_config.exclude_patterns,
78
+ *(config.exclude_patterns or []),
79
+ ]
73
80
  return filter(
74
81
  lambda f: f.suffix.lower() in extensions,
75
82
  iter_files(
@@ -77,7 +84,7 @@ class RobotFrameworkLanguageProvider(LanguageProvider):
77
84
  ignore_files=[ROBOT_IGNORE_FILE, GIT_IGNORE_FILE],
78
85
  include_hidden=False,
79
86
  parent_spec=IgnoreSpec.from_list(
80
- [*DEFAULT_SPEC_RULES, *(config.exclude_patterns or [])],
87
+ [*DEFAULT_SPEC_RULES, *exclude_patterns],
81
88
  folder.uri.to_path(),
82
89
  ),
83
90
  verbose_callback=self.verbose_callback,
@@ -90,4 +97,9 @@ class RobotFrameworkLanguageProvider(LanguageProvider):
90
97
 
91
98
  namespace.analyze()
92
99
 
93
- return namespace.get_diagnostics()
100
+ 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
@@ -1 +0,0 @@
1
- __version__ = "0.96.0"
@@ -1,146 +0,0 @@
1
- from pathlib import Path
2
- from textwrap import indent
3
- from typing import Set, Tuple
4
-
5
- import click
6
-
7
- from robotcode.analyze.config import AnalyzeConfig
8
- from robotcode.core.lsp.types import DiagnosticSeverity
9
- from robotcode.core.text_document import TextDocument
10
- from robotcode.plugin import Application, pass_application
11
- from robotcode.robot.config.loader import (
12
- load_robot_config_from_path,
13
- )
14
- from robotcode.robot.config.utils import get_config_files
15
-
16
- from .__version__ import __version__
17
- from .code_analyzer import CodeAnalyzer
18
-
19
-
20
- @click.group(
21
- add_help_option=True,
22
- invoke_without_command=False,
23
- )
24
- @click.version_option(
25
- version=__version__,
26
- package_name="robotcode.analyze",
27
- prog_name="RobotCode Analyze",
28
- )
29
- @pass_application
30
- def analyze(app: Application) -> None:
31
- """\
32
- The analyze command provides various subcommands for analyzing Robot Framework code.
33
- These subcommands support specialized tasks, such as code analysis, style checking or dependency graphs.
34
- """
35
-
36
-
37
- SEVERITY_COLORS = {
38
- DiagnosticSeverity.ERROR: "red",
39
- DiagnosticSeverity.WARNING: "yellow",
40
- DiagnosticSeverity.INFORMATION: "blue",
41
- DiagnosticSeverity.HINT: "cyan",
42
- }
43
-
44
-
45
- class Statistic:
46
- def __init__(self) -> None:
47
- self.files: Set[TextDocument] = set()
48
- self.errors = 0
49
- self.warnings = 0
50
- self.infos = 0
51
- self.hints = 0
52
-
53
- def __str__(self) -> str:
54
- return (
55
- f"Files: {len(self.files)}, Errors: {self.errors}, Warnings: {self.warnings}, "
56
- f"Infos: {self.infos}, Hints: {self.hints}"
57
- )
58
-
59
-
60
- @analyze.command(
61
- add_help_option=True,
62
- )
63
- @click.version_option(
64
- version=__version__,
65
- package_name="robotcode.analyze",
66
- prog_name="RobotCode Analyze",
67
- )
68
- @click.option(
69
- "-f",
70
- "--filter",
71
- "filter",
72
- type=str,
73
- multiple=True,
74
- help="""\
75
- Glob pattern to filter files to analyze. Can be specified multiple times.
76
- """,
77
- )
78
- @click.argument(
79
- "paths", nargs=-1, type=click.Path(exists=True, dir_okay=True, file_okay=True, readable=True, path_type=Path)
80
- )
81
- @pass_application
82
- def code(app: Application, filter: Tuple[str], paths: Tuple[Path]) -> None:
83
- """\
84
- Performs static code analysis to detect syntax errors, missing keywords or variables,
85
- missing arguments, and more on the given *PATHS*. *PATHS* can be files or directories.
86
- If no PATHS are given, the current directory is used.
87
- """
88
-
89
- config_files, root_folder, _ = get_config_files(
90
- paths,
91
- app.config.config_files,
92
- root_folder=app.config.root,
93
- no_vcs=app.config.no_vcs,
94
- verbose_callback=app.verbose,
95
- )
96
-
97
- try:
98
- robot_config = load_robot_config_from_path(
99
- *config_files, extra_tools={"robotcode-analyze": AnalyzeConfig}, verbose_callback=app.verbose
100
- )
101
-
102
- analyzer_config = robot_config.tool.get("robotcode-analyze", None) if robot_config.tool is not None else None
103
- if analyzer_config is None:
104
- analyzer_config = AnalyzeConfig()
105
-
106
- robot_profile = robot_config.combine_profiles(
107
- *(app.config.profiles or []), verbose_callback=app.verbose, error_callback=app.error
108
- ).evaluated_with_env()
109
-
110
- statistics = Statistic()
111
- for e in CodeAnalyzer(
112
- app=app, config=analyzer_config, robot_profile=robot_profile, root_folder=root_folder
113
- ).run(paths=paths, filter=filter):
114
- statistics.files.add(e.document)
115
-
116
- doc_path = e.document.uri.to_path().relative_to(root_folder) if root_folder else e.document.uri.to_path()
117
- if e.items:
118
-
119
- for item in e.items:
120
- severity = item.severity if item.severity is not None else DiagnosticSeverity.ERROR
121
-
122
- if severity == DiagnosticSeverity.ERROR:
123
- statistics.errors += 1
124
- elif severity == DiagnosticSeverity.WARNING:
125
- statistics.warnings += 1
126
- elif severity == DiagnosticSeverity.INFORMATION:
127
- statistics.infos += 1
128
- elif severity == DiagnosticSeverity.HINT:
129
- statistics.hints += 1
130
-
131
- app.echo(
132
- f"{doc_path}:{item.range.start.line + 1}:{item.range.start.character + 1}: "
133
- + click.style(f"[{severity.name[0]}] {item.code}", fg=SEVERITY_COLORS[severity])
134
- + f": {indent(item.message, prefix=' ').strip()}",
135
- )
136
-
137
- statistics_str = str(statistics)
138
- if statistics.errors > 0:
139
- statistics_str = click.style(statistics_str, fg="red")
140
-
141
- app.echo(statistics_str)
142
-
143
- app.exit(statistics.errors)
144
-
145
- except (TypeError, ValueError) as e:
146
- raise click.ClickException(str(e)) from e