lucidscan 0.5.12__py3-none-any.whl

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.
Files changed (91) hide show
  1. lucidscan/__init__.py +12 -0
  2. lucidscan/bootstrap/__init__.py +26 -0
  3. lucidscan/bootstrap/paths.py +160 -0
  4. lucidscan/bootstrap/platform.py +111 -0
  5. lucidscan/bootstrap/validation.py +76 -0
  6. lucidscan/bootstrap/versions.py +119 -0
  7. lucidscan/cli/__init__.py +50 -0
  8. lucidscan/cli/__main__.py +8 -0
  9. lucidscan/cli/arguments.py +405 -0
  10. lucidscan/cli/commands/__init__.py +64 -0
  11. lucidscan/cli/commands/autoconfigure.py +294 -0
  12. lucidscan/cli/commands/help.py +69 -0
  13. lucidscan/cli/commands/init.py +656 -0
  14. lucidscan/cli/commands/list_scanners.py +59 -0
  15. lucidscan/cli/commands/scan.py +307 -0
  16. lucidscan/cli/commands/serve.py +142 -0
  17. lucidscan/cli/commands/status.py +84 -0
  18. lucidscan/cli/commands/validate.py +105 -0
  19. lucidscan/cli/config_bridge.py +152 -0
  20. lucidscan/cli/exit_codes.py +17 -0
  21. lucidscan/cli/runner.py +284 -0
  22. lucidscan/config/__init__.py +29 -0
  23. lucidscan/config/ignore.py +178 -0
  24. lucidscan/config/loader.py +431 -0
  25. lucidscan/config/models.py +316 -0
  26. lucidscan/config/validation.py +645 -0
  27. lucidscan/core/__init__.py +3 -0
  28. lucidscan/core/domain_runner.py +463 -0
  29. lucidscan/core/git.py +174 -0
  30. lucidscan/core/logging.py +34 -0
  31. lucidscan/core/models.py +207 -0
  32. lucidscan/core/streaming.py +340 -0
  33. lucidscan/core/subprocess_runner.py +164 -0
  34. lucidscan/detection/__init__.py +21 -0
  35. lucidscan/detection/detector.py +154 -0
  36. lucidscan/detection/frameworks.py +270 -0
  37. lucidscan/detection/languages.py +328 -0
  38. lucidscan/detection/tools.py +229 -0
  39. lucidscan/generation/__init__.py +15 -0
  40. lucidscan/generation/config_generator.py +275 -0
  41. lucidscan/generation/package_installer.py +330 -0
  42. lucidscan/mcp/__init__.py +20 -0
  43. lucidscan/mcp/formatter.py +510 -0
  44. lucidscan/mcp/server.py +297 -0
  45. lucidscan/mcp/tools.py +1049 -0
  46. lucidscan/mcp/watcher.py +237 -0
  47. lucidscan/pipeline/__init__.py +17 -0
  48. lucidscan/pipeline/executor.py +187 -0
  49. lucidscan/pipeline/parallel.py +181 -0
  50. lucidscan/plugins/__init__.py +40 -0
  51. lucidscan/plugins/coverage/__init__.py +28 -0
  52. lucidscan/plugins/coverage/base.py +160 -0
  53. lucidscan/plugins/coverage/coverage_py.py +454 -0
  54. lucidscan/plugins/coverage/istanbul.py +411 -0
  55. lucidscan/plugins/discovery.py +107 -0
  56. lucidscan/plugins/enrichers/__init__.py +61 -0
  57. lucidscan/plugins/enrichers/base.py +63 -0
  58. lucidscan/plugins/linters/__init__.py +26 -0
  59. lucidscan/plugins/linters/base.py +125 -0
  60. lucidscan/plugins/linters/biome.py +448 -0
  61. lucidscan/plugins/linters/checkstyle.py +393 -0
  62. lucidscan/plugins/linters/eslint.py +368 -0
  63. lucidscan/plugins/linters/ruff.py +498 -0
  64. lucidscan/plugins/reporters/__init__.py +45 -0
  65. lucidscan/plugins/reporters/base.py +30 -0
  66. lucidscan/plugins/reporters/json_reporter.py +79 -0
  67. lucidscan/plugins/reporters/sarif_reporter.py +303 -0
  68. lucidscan/plugins/reporters/summary_reporter.py +61 -0
  69. lucidscan/plugins/reporters/table_reporter.py +81 -0
  70. lucidscan/plugins/scanners/__init__.py +57 -0
  71. lucidscan/plugins/scanners/base.py +60 -0
  72. lucidscan/plugins/scanners/checkov.py +484 -0
  73. lucidscan/plugins/scanners/opengrep.py +464 -0
  74. lucidscan/plugins/scanners/trivy.py +492 -0
  75. lucidscan/plugins/test_runners/__init__.py +27 -0
  76. lucidscan/plugins/test_runners/base.py +111 -0
  77. lucidscan/plugins/test_runners/jest.py +381 -0
  78. lucidscan/plugins/test_runners/karma.py +481 -0
  79. lucidscan/plugins/test_runners/playwright.py +434 -0
  80. lucidscan/plugins/test_runners/pytest.py +598 -0
  81. lucidscan/plugins/type_checkers/__init__.py +27 -0
  82. lucidscan/plugins/type_checkers/base.py +106 -0
  83. lucidscan/plugins/type_checkers/mypy.py +355 -0
  84. lucidscan/plugins/type_checkers/pyright.py +313 -0
  85. lucidscan/plugins/type_checkers/typescript.py +280 -0
  86. lucidscan-0.5.12.dist-info/METADATA +242 -0
  87. lucidscan-0.5.12.dist-info/RECORD +91 -0
  88. lucidscan-0.5.12.dist-info/WHEEL +5 -0
  89. lucidscan-0.5.12.dist-info/entry_points.txt +34 -0
  90. lucidscan-0.5.12.dist-info/licenses/LICENSE +201 -0
  91. lucidscan-0.5.12.dist-info/top_level.txt +1 -0
@@ -0,0 +1,393 @@
1
+ """Checkstyle linter plugin.
2
+
3
+ Checkstyle is a development tool to help programmers write Java code
4
+ that adheres to a coding standard.
5
+ https://checkstyle.org/
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import shutil
12
+ import subprocess
13
+ import xml.etree.ElementTree as ET
14
+ from pathlib import Path
15
+ from typing import List, Optional
16
+
17
+ from lucidscan.bootstrap.paths import LucidscanPaths
18
+ from lucidscan.bootstrap.versions import get_tool_version
19
+ from lucidscan.core.logging import get_logger
20
+ from lucidscan.core.models import (
21
+ ScanContext,
22
+ Severity,
23
+ ToolDomain,
24
+ UnifiedIssue,
25
+ )
26
+ from lucidscan.core.subprocess_runner import run_with_streaming
27
+ from lucidscan.plugins.linters.base import LinterPlugin
28
+
29
+ LOGGER = get_logger(__name__)
30
+
31
+ # Default version from pyproject.toml [tool.lucidscan.tools]
32
+ DEFAULT_VERSION = get_tool_version("checkstyle")
33
+
34
+ # Checkstyle severity mapping
35
+ SEVERITY_MAP = {
36
+ "error": Severity.HIGH,
37
+ "warning": Severity.MEDIUM,
38
+ "info": Severity.LOW,
39
+ "ignore": Severity.INFO,
40
+ }
41
+
42
+
43
+ class CheckstyleLinter(LinterPlugin):
44
+ """Checkstyle linter plugin for Java code analysis."""
45
+
46
+ def __init__(
47
+ self,
48
+ version: str = DEFAULT_VERSION,
49
+ project_root: Optional[Path] = None,
50
+ ):
51
+ """Initialize CheckstyleLinter.
52
+
53
+ Args:
54
+ version: Checkstyle version to use.
55
+ project_root: Optional project root for tool installation.
56
+ """
57
+ self._version = version
58
+ if project_root:
59
+ self._paths = LucidscanPaths.for_project(project_root)
60
+ self._project_root = project_root
61
+ else:
62
+ self._paths = LucidscanPaths.default()
63
+ self._project_root = None
64
+
65
+ @property
66
+ def name(self) -> str:
67
+ """Plugin identifier."""
68
+ return "checkstyle"
69
+
70
+ @property
71
+ def languages(self) -> List[str]:
72
+ """Supported languages."""
73
+ return ["java"]
74
+
75
+ @property
76
+ def supports_fix(self) -> bool:
77
+ """Checkstyle does not support auto-fix."""
78
+ return False
79
+
80
+ def get_version(self) -> str:
81
+ """Get Checkstyle version."""
82
+ return self._version
83
+
84
+ def _check_java_available(self) -> Optional[Path]:
85
+ """Check if Java is available.
86
+
87
+ Returns:
88
+ Path to java binary or None if not found.
89
+ """
90
+ java_path = shutil.which("java")
91
+ return Path(java_path) if java_path else None
92
+
93
+ def ensure_binary(self) -> Path:
94
+ """Ensure Checkstyle JAR is available.
95
+
96
+ Downloads from GitHub releases if not present.
97
+
98
+ Returns:
99
+ Path to Checkstyle JAR file.
100
+
101
+ Raises:
102
+ FileNotFoundError: If Java is not installed.
103
+ """
104
+ # First check if Java is available
105
+ if not self._check_java_available():
106
+ raise FileNotFoundError(
107
+ "Java is not installed. Checkstyle requires Java.\n"
108
+ "Install Java JDK/JRE to use Checkstyle."
109
+ )
110
+
111
+ jar_dir = self._paths.plugin_bin_dir(self.name, self._version)
112
+ jar_name = f"checkstyle-{self._version}-all.jar"
113
+ jar_path = jar_dir / jar_name
114
+
115
+ if jar_path.exists():
116
+ return jar_path
117
+
118
+ LOGGER.info(f"Downloading Checkstyle {self._version}...")
119
+ jar_dir.mkdir(parents=True, exist_ok=True)
120
+
121
+ self._download_jar(jar_path)
122
+
123
+ LOGGER.info(f"Checkstyle {self._version} installed to {jar_dir}")
124
+ return jar_path
125
+
126
+ def _download_jar(self, target_path: Path) -> None:
127
+ """Download Checkstyle JAR file.
128
+
129
+ Args:
130
+ target_path: Path to save the JAR file.
131
+ """
132
+ import urllib.request
133
+
134
+ url = (
135
+ f"https://github.com/checkstyle/checkstyle/releases/download/"
136
+ f"checkstyle-{self._version}/checkstyle-{self._version}-all.jar"
137
+ )
138
+
139
+ LOGGER.debug(f"Downloading from {url}")
140
+
141
+ # Validate URL scheme and domain for security
142
+ if not url.startswith("https://github.com/"):
143
+ raise ValueError(f"Invalid download URL: {url}")
144
+
145
+ try:
146
+ urllib.request.urlretrieve(url, target_path) # nosec B310 nosemgrep
147
+ except Exception as e:
148
+ raise RuntimeError(f"Failed to download Checkstyle: {e}") from e
149
+
150
+ def lint(self, context: ScanContext) -> List[UnifiedIssue]:
151
+ """Run Checkstyle linting.
152
+
153
+ Args:
154
+ context: Scan context with paths and configuration.
155
+
156
+ Returns:
157
+ List of linting issues.
158
+ """
159
+ try:
160
+ jar_path = self.ensure_binary()
161
+ except FileNotFoundError as e:
162
+ LOGGER.warning(str(e))
163
+ return []
164
+
165
+ java_path = self._check_java_available()
166
+ if not java_path:
167
+ LOGGER.warning("Java not found, skipping Checkstyle")
168
+ return []
169
+
170
+ # Determine config file
171
+ config_file = self._find_config_file(context.project_root)
172
+
173
+ # Build command
174
+ cmd = [
175
+ str(java_path),
176
+ "-jar", str(jar_path),
177
+ "-c", config_file,
178
+ "-f", "xml",
179
+ ]
180
+
181
+ # Find Java source files
182
+ java_files = self._find_java_files(context)
183
+ if not java_files:
184
+ LOGGER.info("No Java files found to check")
185
+ return []
186
+
187
+ cmd.extend(java_files)
188
+
189
+ LOGGER.debug(f"Running: {' '.join(cmd[:10])}...") # Truncate for readability
190
+
191
+ try:
192
+ result = run_with_streaming(
193
+ cmd=cmd,
194
+ cwd=context.project_root,
195
+ tool_name="checkstyle",
196
+ stream_handler=context.stream_handler,
197
+ timeout=120,
198
+ )
199
+ except subprocess.TimeoutExpired:
200
+ LOGGER.warning("Checkstyle timed out after 120 seconds")
201
+ return []
202
+ except Exception as e:
203
+ LOGGER.error(f"Failed to run Checkstyle: {e}")
204
+ return []
205
+
206
+ # Parse XML output
207
+ issues = self._parse_output(result.stdout, context.project_root)
208
+
209
+ LOGGER.info(f"Checkstyle found {len(issues)} issues")
210
+ return issues
211
+
212
+ def _find_config_file(self, project_root: Path) -> str:
213
+ """Find Checkstyle configuration file.
214
+
215
+ Args:
216
+ project_root: Project root directory.
217
+
218
+ Returns:
219
+ Path to config file or built-in config name.
220
+ """
221
+ # Check for custom config files
222
+ custom_configs = [
223
+ "checkstyle.xml",
224
+ ".checkstyle.xml",
225
+ "config/checkstyle/checkstyle.xml",
226
+ ]
227
+
228
+ for config in custom_configs:
229
+ config_path = project_root / config
230
+ if config_path.exists():
231
+ return str(config_path)
232
+
233
+ # Use built-in Google checks as default
234
+ return "/google_checks.xml"
235
+
236
+ def _find_java_files(self, context: ScanContext) -> List[str]:
237
+ """Find Java source files to check.
238
+
239
+ Args:
240
+ context: Scan context.
241
+
242
+ Returns:
243
+ List of Java file paths.
244
+ """
245
+ java_files = []
246
+
247
+ # Search in specified paths or common Java directories
248
+ search_dirs = []
249
+ if context.paths:
250
+ search_dirs = list(context.paths)
251
+ else:
252
+ # Common Java source directories
253
+ for src_dir in ["src", "src/main/java", "src/test/java"]:
254
+ src_path = context.project_root / src_dir
255
+ if src_path.exists():
256
+ search_dirs.append(src_path)
257
+
258
+ if not search_dirs:
259
+ search_dirs = [context.project_root]
260
+
261
+ for search_dir in search_dirs:
262
+ if not search_dir.exists():
263
+ continue
264
+
265
+ for java_file in search_dir.rglob("*.java"):
266
+ # Check if file should be excluded using proper gitignore matching
267
+ if context.ignore_patterns is None or not context.ignore_patterns.matches(
268
+ java_file, context.project_root
269
+ ):
270
+ java_files.append(str(java_file))
271
+
272
+ return java_files
273
+
274
+ def _parse_output(self, output: str, project_root: Path) -> List[UnifiedIssue]:
275
+ """Parse Checkstyle XML output.
276
+
277
+ Args:
278
+ output: XML output from Checkstyle.
279
+ project_root: Project root directory.
280
+
281
+ Returns:
282
+ List of UnifiedIssue objects.
283
+ """
284
+ if not output.strip():
285
+ return []
286
+
287
+ try:
288
+ root = ET.fromstring(output)
289
+ except ET.ParseError as e:
290
+ LOGGER.warning(f"Failed to parse Checkstyle XML output: {e}")
291
+ return []
292
+
293
+ issues = []
294
+
295
+ for file_elem in root.findall(".//file"):
296
+ file_path = file_elem.get("name", "")
297
+
298
+ for error_elem in file_elem.findall("error"):
299
+ issue = self._error_to_issue(error_elem, file_path, project_root)
300
+ if issue:
301
+ issues.append(issue)
302
+
303
+ return issues
304
+
305
+ def _error_to_issue(
306
+ self,
307
+ error_elem: ET.Element,
308
+ file_path: str,
309
+ project_root: Path,
310
+ ) -> Optional[UnifiedIssue]:
311
+ """Convert Checkstyle error element to UnifiedIssue.
312
+
313
+ Args:
314
+ error_elem: XML error element.
315
+ file_path: File path from parent file element.
316
+ project_root: Project root directory.
317
+
318
+ Returns:
319
+ UnifiedIssue or None.
320
+ """
321
+ try:
322
+ line = error_elem.get("line")
323
+ column = error_elem.get("column")
324
+ severity_str = error_elem.get("severity", "error")
325
+ message = error_elem.get("message", "")
326
+ source = error_elem.get("source", "")
327
+
328
+ # Extract rule name from source (e.g., "com.puppycrawl...WhitespaceAfterCheck")
329
+ rule = source.split(".")[-1] if source else ""
330
+
331
+ # Get severity
332
+ severity = SEVERITY_MAP.get(severity_str, Severity.MEDIUM)
333
+
334
+ # Build file path
335
+ path = Path(file_path)
336
+ if not path.is_absolute():
337
+ path = project_root / path
338
+
339
+ # Parse line/column
340
+ line_num = int(line) if line else None
341
+ col_num = int(column) if column else None
342
+
343
+ # Generate deterministic ID
344
+ issue_id = self._generate_issue_id(
345
+ rule, file_path, line_num, col_num, message
346
+ )
347
+
348
+ return UnifiedIssue(
349
+ id=issue_id,
350
+ domain=ToolDomain.LINTING,
351
+ source_tool="checkstyle",
352
+ severity=severity,
353
+ rule_id=rule or "unknown",
354
+ title=f"[{rule}] {message}" if rule else message,
355
+ description=message,
356
+ documentation_url=f"https://checkstyle.org/checks.html#{rule}" if rule else None,
357
+ file_path=path,
358
+ line_start=line_num,
359
+ line_end=line_num,
360
+ column_start=col_num,
361
+ fixable=False, # Checkstyle doesn't support auto-fix
362
+ metadata={
363
+ "source": source,
364
+ "severity_raw": severity_str,
365
+ },
366
+ )
367
+ except Exception as e:
368
+ LOGGER.warning(f"Failed to parse Checkstyle error: {e}")
369
+ return None
370
+
371
+ def _generate_issue_id(
372
+ self,
373
+ rule: str,
374
+ file: str,
375
+ line: Optional[int],
376
+ column: Optional[int],
377
+ message: str,
378
+ ) -> str:
379
+ """Generate deterministic issue ID.
380
+
381
+ Args:
382
+ rule: Check/rule name.
383
+ file: File path.
384
+ line: Line number.
385
+ column: Column number.
386
+ message: Error message.
387
+
388
+ Returns:
389
+ Unique issue ID.
390
+ """
391
+ content = f"{rule}:{file}:{line or 0}:{column or 0}:{message}"
392
+ hash_val = hashlib.sha256(content.encode()).hexdigest()[:12]
393
+ return f"checkstyle-{rule}-{hash_val}" if rule else f"checkstyle-{hash_val}"