robotcode-analyze 2.4.0__tar.gz → 2.5.1__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.
- {robotcode_analyze-2.4.0 → robotcode_analyze-2.5.1}/.gitignore +5 -1
- {robotcode_analyze-2.4.0 → robotcode_analyze-2.5.1}/PKG-INFO +1 -1
- robotcode_analyze-2.5.1/src/robotcode/analyze/__version__.py +1 -0
- robotcode_analyze-2.5.1/src/robotcode/analyze/cache/cli.py +421 -0
- {robotcode_analyze-2.4.0 → robotcode_analyze-2.5.1}/src/robotcode/analyze/cli.py +2 -0
- {robotcode_analyze-2.4.0 → robotcode_analyze-2.5.1}/src/robotcode/analyze/code/cli.py +30 -1
- {robotcode_analyze-2.4.0 → robotcode_analyze-2.5.1}/src/robotcode/analyze/code/code_analyzer.py +6 -0
- {robotcode_analyze-2.4.0 → robotcode_analyze-2.5.1}/src/robotcode/analyze/code/diagnostics_context.py +4 -0
- {robotcode_analyze-2.4.0 → robotcode_analyze-2.5.1}/src/robotcode/analyze/code/robot_framework_language_provider.py +15 -27
- {robotcode_analyze-2.4.0 → robotcode_analyze-2.5.1}/src/robotcode/analyze/config.py +30 -0
- robotcode_analyze-2.4.0/src/robotcode/analyze/__version__.py +0 -1
- {robotcode_analyze-2.4.0 → robotcode_analyze-2.5.1}/README.md +0 -0
- {robotcode_analyze-2.4.0 → robotcode_analyze-2.5.1}/pyproject.toml +0 -0
- {robotcode_analyze-2.4.0 → robotcode_analyze-2.5.1}/src/robotcode/analyze/__init__.py +0 -0
- {robotcode_analyze-2.4.0 → robotcode_analyze-2.5.1}/src/robotcode/analyze/code/language_provider.py +0 -0
- {robotcode_analyze-2.4.0 → robotcode_analyze-2.5.1}/src/robotcode/analyze/hooks.py +0 -0
- {robotcode_analyze-2.4.0 → robotcode_analyze-2.5.1}/src/robotcode/analyze/py.typed +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.5.1"
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Optional, Tuple
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
from robotcode.plugin import Application, OutputFormat, pass_application
|
|
7
|
+
from robotcode.robot.config.loader import load_robot_config_from_path
|
|
8
|
+
from robotcode.robot.config.utils import get_config_files
|
|
9
|
+
from robotcode.robot.diagnostics.data_cache import (
|
|
10
|
+
_LOCK_FILE_NAME,
|
|
11
|
+
CACHE_DIR_NAME,
|
|
12
|
+
CacheSection,
|
|
13
|
+
SqliteDataCache,
|
|
14
|
+
build_cache_dir,
|
|
15
|
+
exclusive_cache_lock,
|
|
16
|
+
resolve_cache_base_path,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from ..config import AnalyzeConfig
|
|
20
|
+
|
|
21
|
+
_SECTION_NAMES = {s.name.lower(): s for s in CacheSection}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _resolve_cache(
|
|
25
|
+
app: Application,
|
|
26
|
+
paths: Tuple[Path, ...],
|
|
27
|
+
) -> Tuple[Path, Optional[SqliteDataCache]]:
|
|
28
|
+
config_files, root_folder, _ = get_config_files(
|
|
29
|
+
paths,
|
|
30
|
+
app.config.config_files,
|
|
31
|
+
root_folder=app.config.root,
|
|
32
|
+
no_vcs=app.config.no_vcs,
|
|
33
|
+
verbose_callback=app.verbose,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
robot_config = load_robot_config_from_path(
|
|
37
|
+
*config_files, extra_tools={"robotcode-analyze": AnalyzeConfig}, verbose_callback=app.verbose
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
analyzer_config = robot_config.tool.get("robotcode-analyze", None) if robot_config.tool is not None else None
|
|
41
|
+
|
|
42
|
+
cache_base_path = root_folder or Path.cwd()
|
|
43
|
+
if analyzer_config is not None and isinstance(analyzer_config, AnalyzeConfig):
|
|
44
|
+
if analyzer_config.cache is not None and analyzer_config.cache.cache_dir is not None:
|
|
45
|
+
cache_base_path = Path(analyzer_config.cache.cache_dir)
|
|
46
|
+
|
|
47
|
+
cache_base_path = resolve_cache_base_path(cache_base_path)
|
|
48
|
+
|
|
49
|
+
cache_dir = build_cache_dir(cache_base_path)
|
|
50
|
+
|
|
51
|
+
if not cache_dir.exists() or not (cache_dir / "cache.db").exists():
|
|
52
|
+
return cache_dir, None
|
|
53
|
+
|
|
54
|
+
from ..__version__ import __version__
|
|
55
|
+
|
|
56
|
+
return cache_dir, SqliteDataCache(cache_dir, app_version=__version__)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _parse_sections(sections: Tuple[str, ...]) -> Optional[Tuple[CacheSection, ...]]:
|
|
60
|
+
if not sections:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
result = []
|
|
64
|
+
for s in sections:
|
|
65
|
+
s_lower = s.lower()
|
|
66
|
+
if s_lower not in _SECTION_NAMES:
|
|
67
|
+
raise click.BadParameter(
|
|
68
|
+
f"Unknown section '{s}'. Choose from: {', '.join(_SECTION_NAMES)}",
|
|
69
|
+
param_hint="'--section'",
|
|
70
|
+
)
|
|
71
|
+
result.append(_SECTION_NAMES[s_lower])
|
|
72
|
+
return tuple(result)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _format_bytes(n: int) -> str:
|
|
76
|
+
if n < 1024:
|
|
77
|
+
return f"{n} B"
|
|
78
|
+
if n < 1024 * 1024:
|
|
79
|
+
return f"{n / 1024:.1f} KB"
|
|
80
|
+
return f"{n / (1024 * 1024):.1f} MB"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@click.group(
|
|
84
|
+
name="cache",
|
|
85
|
+
add_help_option=True,
|
|
86
|
+
invoke_without_command=False,
|
|
87
|
+
)
|
|
88
|
+
def cache_group() -> None:
|
|
89
|
+
"""\
|
|
90
|
+
Manage the RobotCode analysis cache.
|
|
91
|
+
|
|
92
|
+
Provides subcommands to inspect, list, and clear cached data
|
|
93
|
+
(library docs, variables, resources, namespaces).
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@cache_group.command(name="path")
|
|
98
|
+
@click.argument(
|
|
99
|
+
"paths", nargs=-1, type=click.Path(exists=True, dir_okay=True, file_okay=True, readable=True, path_type=Path)
|
|
100
|
+
)
|
|
101
|
+
@pass_application
|
|
102
|
+
def cache_path(app: Application, paths: Tuple[Path, ...]) -> None:
|
|
103
|
+
"""\
|
|
104
|
+
Print the cache directory path.
|
|
105
|
+
|
|
106
|
+
Outputs the resolved cache directory for the current project
|
|
107
|
+
and Python/Robot Framework version combination.
|
|
108
|
+
"""
|
|
109
|
+
cache_dir, db = _resolve_cache(app, paths)
|
|
110
|
+
try:
|
|
111
|
+
app.echo(str(cache_dir))
|
|
112
|
+
finally:
|
|
113
|
+
if db is not None:
|
|
114
|
+
db.close()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@cache_group.command(name="info")
|
|
118
|
+
@click.argument(
|
|
119
|
+
"paths", nargs=-1, type=click.Path(exists=True, dir_okay=True, file_okay=True, readable=True, path_type=Path)
|
|
120
|
+
)
|
|
121
|
+
@pass_application
|
|
122
|
+
def cache_info(app: Application, paths: Tuple[Path, ...]) -> None:
|
|
123
|
+
"""\
|
|
124
|
+
Show cache statistics.
|
|
125
|
+
|
|
126
|
+
Displays the cache directory, database size, app version, and
|
|
127
|
+
per-section entry counts with timestamps.
|
|
128
|
+
"""
|
|
129
|
+
cache_dir, db = _resolve_cache(app, paths)
|
|
130
|
+
|
|
131
|
+
if db is None:
|
|
132
|
+
app.echo(f"Cache directory: {cache_dir}")
|
|
133
|
+
app.echo("No cache database found.")
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
db_path = db.db_path
|
|
138
|
+
db_size = db_path.stat().st_size if db_path.exists() else 0
|
|
139
|
+
|
|
140
|
+
section_data = []
|
|
141
|
+
total_entries = 0
|
|
142
|
+
total_bytes = 0
|
|
143
|
+
for section in CacheSection:
|
|
144
|
+
stats = db.get_section_stats(section)
|
|
145
|
+
total_entries += stats.entry_count
|
|
146
|
+
total_bytes += stats.total_blob_bytes
|
|
147
|
+
section_data.append(
|
|
148
|
+
{
|
|
149
|
+
"section": section.name.lower(),
|
|
150
|
+
"entries": stats.entry_count,
|
|
151
|
+
"size": stats.total_blob_bytes,
|
|
152
|
+
"size_formatted": _format_bytes(stats.total_blob_bytes) if stats.entry_count else "—",
|
|
153
|
+
"created": stats.oldest_created or None,
|
|
154
|
+
"modified": stats.newest_modified or None,
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
if app.config.output_format is None or app.config.output_format == OutputFormat.TEXT:
|
|
159
|
+
if app.colored and app.has_rich:
|
|
160
|
+
lines = [
|
|
161
|
+
f"- **Directory:** {cache_dir}",
|
|
162
|
+
f"- **Database:** {db_path.name} ({_format_bytes(db_size)})",
|
|
163
|
+
f"- **Version:** {db.app_version or '(unknown)'}",
|
|
164
|
+
"",
|
|
165
|
+
"| Section | Entries | Size | Created | Modified |",
|
|
166
|
+
"|---|---:|---:|---|---|",
|
|
167
|
+
]
|
|
168
|
+
for s in section_data:
|
|
169
|
+
lines.append(
|
|
170
|
+
f"| {s['section']} | {s['entries']} | {s['size_formatted']}"
|
|
171
|
+
f" | {s['created'] or '—'} | {s['modified'] or '—'} |"
|
|
172
|
+
)
|
|
173
|
+
lines.append(f"| **Total** | **{total_entries}** | **{_format_bytes(total_bytes)}** | | |")
|
|
174
|
+
app.echo_as_markdown("\n".join(lines))
|
|
175
|
+
else:
|
|
176
|
+
app.echo(f" Directory: {cache_dir}")
|
|
177
|
+
app.echo(f" Database: {db_path.name} ({_format_bytes(db_size)})")
|
|
178
|
+
app.echo(f" Version: {db.app_version or '(unknown)'}")
|
|
179
|
+
app.echo("")
|
|
180
|
+
header = f" {'Section':<12} {'Entries':>7} {'Size':>10} {'Created':19} {'Modified':19}"
|
|
181
|
+
app.echo(header)
|
|
182
|
+
app.echo(f" {'─' * (len(header) - 2)}")
|
|
183
|
+
for s in section_data:
|
|
184
|
+
app.echo(
|
|
185
|
+
f" {s['section']:<12} {s['entries']:>7} {s['size_formatted']:>10}"
|
|
186
|
+
f" {(s['created'] or '—'):19} {(s['modified'] or '—'):19}"
|
|
187
|
+
)
|
|
188
|
+
app.echo(f" {'─' * (len(header) - 2)}")
|
|
189
|
+
app.echo(f" {'Total':<12} {total_entries:>7} {_format_bytes(total_bytes):>10}")
|
|
190
|
+
else:
|
|
191
|
+
app.print_data(
|
|
192
|
+
{
|
|
193
|
+
"directory": str(cache_dir),
|
|
194
|
+
"database": db_path.name,
|
|
195
|
+
"database_size": db_size,
|
|
196
|
+
"version": db.app_version or "",
|
|
197
|
+
"sections": [
|
|
198
|
+
{k: v for k, v in s.items() if k != "size_formatted" and v is not None} for s in section_data
|
|
199
|
+
],
|
|
200
|
+
"total_entries": total_entries,
|
|
201
|
+
"total_size": total_bytes,
|
|
202
|
+
}
|
|
203
|
+
)
|
|
204
|
+
finally:
|
|
205
|
+
db.close()
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@cache_group.command(name="list")
|
|
209
|
+
@click.option(
|
|
210
|
+
"-s",
|
|
211
|
+
"--section",
|
|
212
|
+
"sections",
|
|
213
|
+
multiple=True,
|
|
214
|
+
metavar="SECTION",
|
|
215
|
+
help="Filter by section (library, variables, resource, namespace). Can be specified multiple times.",
|
|
216
|
+
)
|
|
217
|
+
@click.option(
|
|
218
|
+
"-p",
|
|
219
|
+
"--pattern",
|
|
220
|
+
"patterns",
|
|
221
|
+
multiple=True,
|
|
222
|
+
metavar="PATTERN",
|
|
223
|
+
help="Filter entries by glob pattern (e.g. 'robot.*', '*BuiltIn*'). Can be specified multiple times.",
|
|
224
|
+
)
|
|
225
|
+
@click.argument(
|
|
226
|
+
"paths", nargs=-1, type=click.Path(exists=True, dir_okay=True, file_okay=True, readable=True, path_type=Path)
|
|
227
|
+
)
|
|
228
|
+
@pass_application
|
|
229
|
+
def cache_list(app: Application, sections: Tuple[str, ...], patterns: Tuple[str, ...], paths: Tuple[Path, ...]) -> None:
|
|
230
|
+
"""\
|
|
231
|
+
List cached entries.
|
|
232
|
+
|
|
233
|
+
Shows all entries in the cache with their timestamps and sizes.
|
|
234
|
+
Use --section to filter by specific cache sections.
|
|
235
|
+
Use --pattern to filter entries by glob pattern.
|
|
236
|
+
"""
|
|
237
|
+
from fnmatch import fnmatch
|
|
238
|
+
|
|
239
|
+
_, db = _resolve_cache(app, paths)
|
|
240
|
+
|
|
241
|
+
if db is None:
|
|
242
|
+
app.echo("No cache database found.")
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
selected = _parse_sections(sections)
|
|
247
|
+
target_sections = selected if selected else tuple(CacheSection)
|
|
248
|
+
|
|
249
|
+
def _matches(name: str) -> bool:
|
|
250
|
+
if not patterns:
|
|
251
|
+
return True
|
|
252
|
+
return any(fnmatch(name, p) for p in patterns)
|
|
253
|
+
|
|
254
|
+
if app.config.output_format is None or app.config.output_format == OutputFormat.TEXT:
|
|
255
|
+
if app.colored and app.has_rich:
|
|
256
|
+
lines: list[str] = []
|
|
257
|
+
for section in target_sections:
|
|
258
|
+
entries = [e for e in db.list_entries(section) if _matches(e.entry_name)]
|
|
259
|
+
if not entries:
|
|
260
|
+
continue
|
|
261
|
+
lines.append(f"### {section.name.lower()} ({len(entries)} entries)")
|
|
262
|
+
lines.append("")
|
|
263
|
+
lines.append("| Name | Size | Created | Modified |")
|
|
264
|
+
lines.append("|---|---:|---|---|")
|
|
265
|
+
for entry in entries:
|
|
266
|
+
size = _format_bytes(entry.meta_bytes + entry.data_bytes)
|
|
267
|
+
created = entry.created_at or "—"
|
|
268
|
+
modified = entry.modified_at or "—"
|
|
269
|
+
lines.append(f"| {entry.entry_name} | {size} | {created} | {modified} |")
|
|
270
|
+
lines.append("")
|
|
271
|
+
if lines:
|
|
272
|
+
app.echo_as_markdown("\n".join(lines))
|
|
273
|
+
else:
|
|
274
|
+
app.echo("No entries found.")
|
|
275
|
+
else:
|
|
276
|
+
found = False
|
|
277
|
+
for section in target_sections:
|
|
278
|
+
entries = [e for e in db.list_entries(section) if _matches(e.entry_name)]
|
|
279
|
+
if not entries:
|
|
280
|
+
continue
|
|
281
|
+
found = True
|
|
282
|
+
app.echo(f"[{section.name.lower()}] ({len(entries)} entries)")
|
|
283
|
+
for entry in entries:
|
|
284
|
+
size = _format_bytes(entry.meta_bytes + entry.data_bytes)
|
|
285
|
+
created = entry.created_at or "—"
|
|
286
|
+
modified = entry.modified_at or "—"
|
|
287
|
+
app.echo(f" {entry.entry_name} size={size} created={created} modified={modified}")
|
|
288
|
+
app.echo("")
|
|
289
|
+
if not found:
|
|
290
|
+
app.echo("No entries found.")
|
|
291
|
+
else:
|
|
292
|
+
result: dict[str, list[dict[str, object]]] = {}
|
|
293
|
+
for section in target_sections:
|
|
294
|
+
entries = [e for e in db.list_entries(section) if _matches(e.entry_name)]
|
|
295
|
+
if entries:
|
|
296
|
+
result[section.name.lower()] = [
|
|
297
|
+
{
|
|
298
|
+
k: v
|
|
299
|
+
for k, v in {
|
|
300
|
+
"name": e.entry_name,
|
|
301
|
+
"size": e.meta_bytes + e.data_bytes,
|
|
302
|
+
"created": e.created_at,
|
|
303
|
+
"modified": e.modified_at,
|
|
304
|
+
}.items()
|
|
305
|
+
if v is not None
|
|
306
|
+
}
|
|
307
|
+
for e in entries
|
|
308
|
+
]
|
|
309
|
+
app.print_data(result)
|
|
310
|
+
finally:
|
|
311
|
+
db.close()
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@cache_group.command(name="clear")
|
|
315
|
+
@click.option(
|
|
316
|
+
"-s",
|
|
317
|
+
"--section",
|
|
318
|
+
"sections",
|
|
319
|
+
multiple=True,
|
|
320
|
+
metavar="SECTION",
|
|
321
|
+
help="Clear only specific sections (library, variables, resource, namespace). Can be specified multiple times.",
|
|
322
|
+
)
|
|
323
|
+
@click.argument(
|
|
324
|
+
"paths", nargs=-1, type=click.Path(exists=True, dir_okay=True, file_okay=True, readable=True, path_type=Path)
|
|
325
|
+
)
|
|
326
|
+
@pass_application
|
|
327
|
+
def cache_clear(app: Application, sections: Tuple[str, ...], paths: Tuple[Path, ...]) -> None:
|
|
328
|
+
"""\
|
|
329
|
+
Clear the analysis cache.
|
|
330
|
+
|
|
331
|
+
Removes cached entries from the database. By default clears all sections.
|
|
332
|
+
Use --section to clear specific sections only.
|
|
333
|
+
"""
|
|
334
|
+
_, db = _resolve_cache(app, paths)
|
|
335
|
+
|
|
336
|
+
if db is None:
|
|
337
|
+
app.echo("No cache database found.")
|
|
338
|
+
return
|
|
339
|
+
|
|
340
|
+
try:
|
|
341
|
+
selected = _parse_sections(sections)
|
|
342
|
+
|
|
343
|
+
if selected:
|
|
344
|
+
total = 0
|
|
345
|
+
for section in selected:
|
|
346
|
+
count = db.clear_section(section)
|
|
347
|
+
total += count
|
|
348
|
+
app.echo(f"Cleared {count} entries from {section.name.lower()}.")
|
|
349
|
+
else:
|
|
350
|
+
total = db.clear_all()
|
|
351
|
+
|
|
352
|
+
app.echo(f"Removed {total} entries total.")
|
|
353
|
+
finally:
|
|
354
|
+
db.close()
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _resolve_cache_root(
|
|
358
|
+
app: Application,
|
|
359
|
+
paths: Tuple[Path, ...],
|
|
360
|
+
) -> Path:
|
|
361
|
+
config_files, root_folder, _ = get_config_files(
|
|
362
|
+
paths,
|
|
363
|
+
app.config.config_files,
|
|
364
|
+
root_folder=app.config.root,
|
|
365
|
+
no_vcs=app.config.no_vcs,
|
|
366
|
+
verbose_callback=app.verbose,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
robot_config = load_robot_config_from_path(
|
|
370
|
+
*config_files, extra_tools={"robotcode-analyze": AnalyzeConfig}, verbose_callback=app.verbose
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
analyzer_config = robot_config.tool.get("robotcode-analyze", None) if robot_config.tool is not None else None
|
|
374
|
+
|
|
375
|
+
cache_base_path = root_folder or Path.cwd()
|
|
376
|
+
if analyzer_config is not None and isinstance(analyzer_config, AnalyzeConfig):
|
|
377
|
+
if analyzer_config.cache is not None and analyzer_config.cache.cache_dir is not None:
|
|
378
|
+
cache_base_path = Path(analyzer_config.cache.cache_dir)
|
|
379
|
+
|
|
380
|
+
cache_base_path = resolve_cache_base_path(cache_base_path)
|
|
381
|
+
|
|
382
|
+
return cache_base_path / CACHE_DIR_NAME
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
@cache_group.command(name="prune")
|
|
386
|
+
@click.option("--force", is_flag=True, help="Force prune even if cache is in use by another process.")
|
|
387
|
+
@click.argument(
|
|
388
|
+
"paths", nargs=-1, type=click.Path(exists=True, dir_okay=True, file_okay=True, readable=True, path_type=Path)
|
|
389
|
+
)
|
|
390
|
+
@pass_application
|
|
391
|
+
def cache_prune(app: Application, force: bool, paths: Tuple[Path, ...]) -> None:
|
|
392
|
+
"""\
|
|
393
|
+
Remove the entire cache directory.
|
|
394
|
+
|
|
395
|
+
Deletes the .robotcode_cache directory and all its contents,
|
|
396
|
+
including caches for all Python and Robot Framework versions.
|
|
397
|
+
"""
|
|
398
|
+
import shutil
|
|
399
|
+
|
|
400
|
+
cache_root = _resolve_cache_root(app, paths)
|
|
401
|
+
|
|
402
|
+
if not cache_root.exists():
|
|
403
|
+
app.echo(f"Cache directory does not exist: {cache_root}")
|
|
404
|
+
return
|
|
405
|
+
|
|
406
|
+
if not force:
|
|
407
|
+
locked_dirs = []
|
|
408
|
+
for lock_file in cache_root.rglob(_LOCK_FILE_NAME):
|
|
409
|
+
with exclusive_cache_lock(lock_file.parent) as acquired:
|
|
410
|
+
if not acquired:
|
|
411
|
+
locked_dirs.append(lock_file.parent)
|
|
412
|
+
|
|
413
|
+
if locked_dirs:
|
|
414
|
+
app.echo("Cannot prune: cache is in use by another process.")
|
|
415
|
+
for d in locked_dirs:
|
|
416
|
+
app.echo(f" Locked: {d}")
|
|
417
|
+
app.echo("Use --force to override.")
|
|
418
|
+
return
|
|
419
|
+
|
|
420
|
+
shutil.rmtree(cache_root)
|
|
421
|
+
app.echo(f"Removed {cache_root}")
|
|
@@ -3,6 +3,7 @@ import click
|
|
|
3
3
|
from robotcode.plugin import Application, pass_application
|
|
4
4
|
|
|
5
5
|
from .__version__ import __version__
|
|
6
|
+
from .cache.cli import cache_group
|
|
6
7
|
from .code.cli import code
|
|
7
8
|
|
|
8
9
|
|
|
@@ -23,4 +24,5 @@ def analyze(app: Application) -> None:
|
|
|
23
24
|
"""
|
|
24
25
|
|
|
25
26
|
|
|
27
|
+
analyze.add_command(cache_group)
|
|
26
28
|
analyze.add_command(code)
|
|
@@ -19,7 +19,7 @@ from robotcode.robot.config.loader import (
|
|
|
19
19
|
from robotcode.robot.config.utils import get_config_files
|
|
20
20
|
|
|
21
21
|
from ..__version__ import __version__
|
|
22
|
-
from ..config import AnalyzeConfig, ExitCodeMask, ModifiersConfig
|
|
22
|
+
from ..config import AnalyzeConfig, CacheConfig, CodeConfig, ExitCodeMask, ModifiersConfig
|
|
23
23
|
from .code_analyzer import CodeAnalyzer, DocumentDiagnosticReport, FolderDiagnosticReport
|
|
24
24
|
|
|
25
25
|
SEVERITY_COLORS = {
|
|
@@ -270,6 +270,18 @@ def _validate_load_library_timeout(ctx: click.Context, param: click.Option, valu
|
|
|
270
270
|
"Must be > 0. Overrides config file and environment variable when set."
|
|
271
271
|
),
|
|
272
272
|
)
|
|
273
|
+
@click.option(
|
|
274
|
+
"--collect-unused/--no-collect-unused",
|
|
275
|
+
default=None,
|
|
276
|
+
help="Enable or disable collection of unused keyword and unused variable diagnostics. "
|
|
277
|
+
"Overrides the config file setting when specified.",
|
|
278
|
+
)
|
|
279
|
+
@click.option(
|
|
280
|
+
"--cache-namespaces/--no-cache-namespaces",
|
|
281
|
+
default=None,
|
|
282
|
+
help="Enable or disable caching of fully analyzed namespace data to disk. "
|
|
283
|
+
"Can speed up startup for large projects by skipping re-analysis of unchanged files.",
|
|
284
|
+
)
|
|
273
285
|
@click.argument(
|
|
274
286
|
"paths", nargs=-1, type=click.Path(exists=True, dir_okay=True, file_okay=True, readable=True, path_type=Path)
|
|
275
287
|
)
|
|
@@ -289,6 +301,8 @@ def code(
|
|
|
289
301
|
extend_exit_code_mask: ExitCodeMask,
|
|
290
302
|
paths: Tuple[Path],
|
|
291
303
|
load_library_timeout: Optional[int],
|
|
304
|
+
collect_unused: Optional[bool],
|
|
305
|
+
cache_namespaces: Optional[bool],
|
|
292
306
|
) -> None:
|
|
293
307
|
"""\
|
|
294
308
|
Performs static code analysis to identify potential issues in the specified *PATHS*. The analysis detects syntax
|
|
@@ -393,6 +407,16 @@ def code(
|
|
|
393
407
|
if load_library_timeout is not None:
|
|
394
408
|
analyzer_config.load_library_timeout = load_library_timeout
|
|
395
409
|
|
|
410
|
+
if collect_unused is not None:
|
|
411
|
+
if analyzer_config.code is None:
|
|
412
|
+
analyzer_config.code = CodeConfig()
|
|
413
|
+
analyzer_config.code.collect_unused = collect_unused
|
|
414
|
+
|
|
415
|
+
if cache_namespaces is not None:
|
|
416
|
+
if analyzer_config.cache is None:
|
|
417
|
+
analyzer_config.cache = CacheConfig()
|
|
418
|
+
analyzer_config.cache.cache_namespaces = cache_namespaces
|
|
419
|
+
|
|
396
420
|
app.verbose(f"Using analyzer_config: {analyzer_config}")
|
|
397
421
|
app.verbose(f"Using exit code mask: {mask}")
|
|
398
422
|
|
|
@@ -403,6 +427,11 @@ def code(
|
|
|
403
427
|
analysis_config=analyzer_config.to_workspace_analysis_config(),
|
|
404
428
|
robot_profile=robot_profile,
|
|
405
429
|
root_folder=root_folder,
|
|
430
|
+
collect_unused=bool(
|
|
431
|
+
analyzer_config.code.collect_unused
|
|
432
|
+
if analyzer_config.code is not None and analyzer_config.code.collect_unused is not None
|
|
433
|
+
else False
|
|
434
|
+
),
|
|
406
435
|
)
|
|
407
436
|
try:
|
|
408
437
|
for e in analyzer.run(paths=paths, filter=filter):
|
{robotcode_analyze-2.4.0 → robotcode_analyze-2.5.1}/src/robotcode/analyze/code/code_analyzer.py
RENAMED
|
@@ -35,9 +35,11 @@ class CodeAnalyzer(DiagnosticsContext):
|
|
|
35
35
|
analysis_config: WorkspaceAnalysisConfig,
|
|
36
36
|
robot_profile: RobotBaseProfile,
|
|
37
37
|
root_folder: Optional[Path],
|
|
38
|
+
collect_unused: bool = False,
|
|
38
39
|
):
|
|
39
40
|
self.app = app
|
|
40
41
|
self._analysis_config = analysis_config or WorkspaceAnalysisConfig()
|
|
42
|
+
self._collect_unused = collect_unused
|
|
41
43
|
|
|
42
44
|
self._robot_profile = robot_profile
|
|
43
45
|
self._root_folder = root_folder if root_folder is not None else Path.cwd()
|
|
@@ -62,6 +64,10 @@ class CodeAnalyzer(DiagnosticsContext):
|
|
|
62
64
|
def analysis_config(self) -> WorkspaceAnalysisConfig:
|
|
63
65
|
return self._analysis_config
|
|
64
66
|
|
|
67
|
+
@property
|
|
68
|
+
def collect_unused(self) -> bool:
|
|
69
|
+
return self._collect_unused
|
|
70
|
+
|
|
65
71
|
@property
|
|
66
72
|
def profile(self) -> RobotBaseProfile:
|
|
67
73
|
return self._robot_profile
|
|
@@ -48,6 +48,10 @@ class DiagnosticsContext(ABC):
|
|
|
48
48
|
@abstractmethod
|
|
49
49
|
def analysis_config(self) -> WorkspaceAnalysisConfig: ...
|
|
50
50
|
|
|
51
|
+
@property
|
|
52
|
+
@abstractmethod
|
|
53
|
+
def collect_unused(self) -> bool: ...
|
|
54
|
+
|
|
51
55
|
@property
|
|
52
56
|
@abstractmethod
|
|
53
57
|
def profile(self) -> RobotBaseProfile: ...
|
|
@@ -57,8 +57,8 @@ class RobotFrameworkLanguageProvider(LanguageProvider):
|
|
|
57
57
|
self.diagnostics_context.diagnostics.folder_analyzers.add(self.analyze_folder)
|
|
58
58
|
self.diagnostics_context.diagnostics.document_analyzers.add(self.analyze_document)
|
|
59
59
|
self.diagnostics_context.diagnostics.document_collectors.add(self.collect_diagnostics)
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
self.diagnostics_context.diagnostics.document_collectors.add(self.collect_unused_keywords)
|
|
61
|
+
self.diagnostics_context.diagnostics.document_collectors.add(self.collect_unused_variables)
|
|
62
62
|
|
|
63
63
|
def _update_python_path(self) -> None:
|
|
64
64
|
root_path = (
|
|
@@ -121,27 +121,20 @@ class RobotFrameworkLanguageProvider(LanguageProvider):
|
|
|
121
121
|
def collect_diagnostics(self, sender: Any, document: TextDocument) -> Optional[List[Diagnostic]]:
|
|
122
122
|
namespace = self._document_cache.get_namespace(document)
|
|
123
123
|
|
|
124
|
-
return self._document_cache.get_diagnostic_modifier(document).modify_diagnostics(namespace.
|
|
124
|
+
return self._document_cache.get_diagnostic_modifier(document).modify_diagnostics(namespace.diagnostics)
|
|
125
125
|
|
|
126
126
|
def collect_unused_keywords(self, sender: Any, document: TextDocument) -> Optional[List[Diagnostic]]:
|
|
127
|
+
if not self.diagnostics_context.collect_unused:
|
|
128
|
+
return None
|
|
129
|
+
|
|
127
130
|
namespace = self._document_cache.get_namespace(document)
|
|
128
131
|
|
|
129
|
-
|
|
130
|
-
[document]
|
|
131
|
-
if self._document_cache.get_document_type(document) != DocumentType.RESOURCE
|
|
132
|
-
else self.diagnostics_context.workspace.documents.documents
|
|
133
|
-
)
|
|
132
|
+
project_index = self._document_cache.get_project_index(document)
|
|
134
133
|
|
|
135
134
|
result: List[Diagnostic] = []
|
|
136
135
|
|
|
137
|
-
for kw in
|
|
138
|
-
|
|
139
|
-
for doc in documents:
|
|
140
|
-
refs = self._document_cache.get_namespace(doc).get_keyword_references()
|
|
141
|
-
if refs.get(kw):
|
|
142
|
-
has_reference = True
|
|
143
|
-
break
|
|
144
|
-
if not has_reference:
|
|
136
|
+
for kw in namespace.library_doc.keywords.values():
|
|
137
|
+
if not project_index.find_keyword_references(kw):
|
|
145
138
|
result.append(
|
|
146
139
|
Diagnostic(
|
|
147
140
|
range=kw.name_range,
|
|
@@ -156,11 +149,15 @@ class RobotFrameworkLanguageProvider(LanguageProvider):
|
|
|
156
149
|
return result
|
|
157
150
|
|
|
158
151
|
def collect_unused_variables(self, sender: Any, document: TextDocument) -> Optional[List[Diagnostic]]:
|
|
152
|
+
if not self.diagnostics_context.collect_unused:
|
|
153
|
+
return None
|
|
154
|
+
|
|
159
155
|
result: List[Diagnostic] = []
|
|
160
156
|
|
|
161
157
|
namespace = self._document_cache.get_namespace(document)
|
|
158
|
+
project_index = self._document_cache.get_project_index(document)
|
|
162
159
|
|
|
163
|
-
for var, locations in
|
|
160
|
+
for var, locations in namespace.variable_references.items():
|
|
164
161
|
if var.type in (
|
|
165
162
|
VariableDefinitionType.LIBRARY_ARGUMENT,
|
|
166
163
|
VariableDefinitionType.ENVIRONMENT_VARIABLE,
|
|
@@ -182,16 +179,7 @@ class RobotFrameworkLanguageProvider(LanguageProvider):
|
|
|
182
179
|
)
|
|
183
180
|
and self._document_cache.get_document_type(document) == DocumentType.RESOURCE
|
|
184
181
|
):
|
|
185
|
-
|
|
186
|
-
self.verbose_callback(f"Checking variable '{var.name}' {var.type} for usage. {document.uri}")
|
|
187
|
-
self.verbose_callback(
|
|
188
|
-
f"Searching references in {len(self.diagnostics_context.workspace.documents)} documents."
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
has_reference = any(
|
|
192
|
-
len(self._document_cache.get_namespace(doc).get_variable_references().get(var, set())) > 0
|
|
193
|
-
for doc in self.diagnostics_context.workspace.documents.documents
|
|
194
|
-
)
|
|
182
|
+
has_reference = bool(project_index.find_variable_references(var))
|
|
195
183
|
|
|
196
184
|
if not has_reference:
|
|
197
185
|
result.append(
|
|
@@ -197,6 +197,21 @@ class CacheConfig(BaseOptions):
|
|
|
197
197
|
description="Extend the ignore arguments for library settings."
|
|
198
198
|
)
|
|
199
199
|
|
|
200
|
+
cache_namespaces: Optional[bool] = field(
|
|
201
|
+
description="""\
|
|
202
|
+
Enable or disable caching of fully analyzed namespace data to disk.
|
|
203
|
+
Can speed up startup for large projects by skipping re-analysis of unchanged files.
|
|
204
|
+
Defaults to enabled.
|
|
205
|
+
|
|
206
|
+
Examples:
|
|
207
|
+
|
|
208
|
+
```toml
|
|
209
|
+
[tool.robotcode-analyze.cache]
|
|
210
|
+
cache_namespaces = false
|
|
211
|
+
```
|
|
212
|
+
""",
|
|
213
|
+
)
|
|
214
|
+
|
|
200
215
|
|
|
201
216
|
class ExitCodeMask(IntFlag):
|
|
202
217
|
NONE = 0
|
|
@@ -247,6 +262,20 @@ class CodeConfig(BaseOptions):
|
|
|
247
262
|
)
|
|
248
263
|
extend_exit_code_mask: Optional[ExitCodeMaskList] = field(description="Extend the exit code mask setting.")
|
|
249
264
|
|
|
265
|
+
collect_unused: Optional[bool] = field(
|
|
266
|
+
description="""\
|
|
267
|
+
Enables collection of unused keyword and unused variable diagnostics.
|
|
268
|
+
By default this is disabled. Set to `true` to report unused keywords and variables.
|
|
269
|
+
|
|
270
|
+
Examples:
|
|
271
|
+
|
|
272
|
+
```toml
|
|
273
|
+
[tool.robotcode-analyze.code]
|
|
274
|
+
collect_unused = true
|
|
275
|
+
```
|
|
276
|
+
""",
|
|
277
|
+
)
|
|
278
|
+
|
|
250
279
|
|
|
251
280
|
@dataclass
|
|
252
281
|
class AnalyzeConfig(BaseOptions):
|
|
@@ -358,6 +387,7 @@ class AnalyzeConfig(BaseOptions):
|
|
|
358
387
|
ignored_libraries=self.cache.ignored_libraries or [],
|
|
359
388
|
ignored_variables=self.cache.ignored_variables or [],
|
|
360
389
|
ignore_arguments_for_library=self.cache.ignore_arguments_for_library or [],
|
|
390
|
+
cache_namespaces=(self.cache.cache_namespaces if self.cache.cache_namespaces is not None else True),
|
|
361
391
|
)
|
|
362
392
|
if self.cache is not None
|
|
363
393
|
else WorkspaceCacheConfig()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.4.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{robotcode_analyze-2.4.0 → robotcode_analyze-2.5.1}/src/robotcode/analyze/code/language_provider.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|