nc1709 1.15.4__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.
- nc1709/__init__.py +13 -0
- nc1709/agent/__init__.py +36 -0
- nc1709/agent/core.py +505 -0
- nc1709/agent/mcp_bridge.py +245 -0
- nc1709/agent/permissions.py +298 -0
- nc1709/agent/tools/__init__.py +21 -0
- nc1709/agent/tools/base.py +440 -0
- nc1709/agent/tools/bash_tool.py +367 -0
- nc1709/agent/tools/file_tools.py +454 -0
- nc1709/agent/tools/notebook_tools.py +516 -0
- nc1709/agent/tools/search_tools.py +322 -0
- nc1709/agent/tools/task_tool.py +284 -0
- nc1709/agent/tools/web_tools.py +555 -0
- nc1709/agents/__init__.py +17 -0
- nc1709/agents/auto_fix.py +506 -0
- nc1709/agents/test_generator.py +507 -0
- nc1709/checkpoints.py +372 -0
- nc1709/cli.py +3380 -0
- nc1709/cli_ui.py +1080 -0
- nc1709/cognitive/__init__.py +149 -0
- nc1709/cognitive/anticipation.py +594 -0
- nc1709/cognitive/context_engine.py +1046 -0
- nc1709/cognitive/council.py +824 -0
- nc1709/cognitive/learning.py +761 -0
- nc1709/cognitive/router.py +583 -0
- nc1709/cognitive/system.py +519 -0
- nc1709/config.py +155 -0
- nc1709/custom_commands.py +300 -0
- nc1709/executor.py +333 -0
- nc1709/file_controller.py +354 -0
- nc1709/git_integration.py +308 -0
- nc1709/github_integration.py +477 -0
- nc1709/image_input.py +446 -0
- nc1709/linting.py +519 -0
- nc1709/llm_adapter.py +667 -0
- nc1709/logger.py +192 -0
- nc1709/mcp/__init__.py +18 -0
- nc1709/mcp/client.py +370 -0
- nc1709/mcp/manager.py +407 -0
- nc1709/mcp/protocol.py +210 -0
- nc1709/mcp/server.py +473 -0
- nc1709/memory/__init__.py +20 -0
- nc1709/memory/embeddings.py +325 -0
- nc1709/memory/indexer.py +474 -0
- nc1709/memory/sessions.py +432 -0
- nc1709/memory/vector_store.py +451 -0
- nc1709/models/__init__.py +86 -0
- nc1709/models/detector.py +377 -0
- nc1709/models/formats.py +315 -0
- nc1709/models/manager.py +438 -0
- nc1709/models/registry.py +497 -0
- nc1709/performance/__init__.py +343 -0
- nc1709/performance/cache.py +705 -0
- nc1709/performance/pipeline.py +611 -0
- nc1709/performance/tiering.py +543 -0
- nc1709/plan_mode.py +362 -0
- nc1709/plugins/__init__.py +17 -0
- nc1709/plugins/agents/__init__.py +18 -0
- nc1709/plugins/agents/django_agent.py +912 -0
- nc1709/plugins/agents/docker_agent.py +623 -0
- nc1709/plugins/agents/fastapi_agent.py +887 -0
- nc1709/plugins/agents/git_agent.py +731 -0
- nc1709/plugins/agents/nextjs_agent.py +867 -0
- nc1709/plugins/base.py +359 -0
- nc1709/plugins/manager.py +411 -0
- nc1709/plugins/registry.py +337 -0
- nc1709/progress.py +443 -0
- nc1709/prompts/__init__.py +22 -0
- nc1709/prompts/agent_system.py +180 -0
- nc1709/prompts/task_prompts.py +340 -0
- nc1709/prompts/unified_prompt.py +133 -0
- nc1709/reasoning_engine.py +541 -0
- nc1709/remote_client.py +266 -0
- nc1709/shell_completions.py +349 -0
- nc1709/slash_commands.py +649 -0
- nc1709/task_classifier.py +408 -0
- nc1709/version_check.py +177 -0
- nc1709/web/__init__.py +8 -0
- nc1709/web/server.py +950 -0
- nc1709/web/templates/index.html +1127 -0
- nc1709-1.15.4.dist-info/METADATA +858 -0
- nc1709-1.15.4.dist-info/RECORD +86 -0
- nc1709-1.15.4.dist-info/WHEEL +5 -0
- nc1709-1.15.4.dist-info/entry_points.txt +2 -0
- nc1709-1.15.4.dist-info/licenses/LICENSE +9 -0
- nc1709-1.15.4.dist-info/top_level.txt +1 -0
nc1709/linting.py
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Linting Integration for NC1709
|
|
3
|
+
|
|
4
|
+
Provides linting capabilities:
|
|
5
|
+
- Auto-detect and run appropriate linters
|
|
6
|
+
- Parse linter output
|
|
7
|
+
- Integrate with AI for auto-fixing
|
|
8
|
+
- Support for multiple languages
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import subprocess
|
|
13
|
+
import json
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional, List, Dict, Any, Tuple
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from enum import Enum
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LintSeverity(Enum):
|
|
21
|
+
"""Severity level of lint issues"""
|
|
22
|
+
ERROR = "error"
|
|
23
|
+
WARNING = "warning"
|
|
24
|
+
INFO = "info"
|
|
25
|
+
HINT = "hint"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class LintIssue:
|
|
30
|
+
"""A single linting issue"""
|
|
31
|
+
file: str
|
|
32
|
+
line: int
|
|
33
|
+
column: int
|
|
34
|
+
severity: LintSeverity
|
|
35
|
+
message: str
|
|
36
|
+
rule: Optional[str] = None
|
|
37
|
+
source: Optional[str] = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class LintResult:
|
|
42
|
+
"""Result of running a linter"""
|
|
43
|
+
success: bool
|
|
44
|
+
linter: str
|
|
45
|
+
issues: List[LintIssue] = field(default_factory=list)
|
|
46
|
+
error_count: int = 0
|
|
47
|
+
warning_count: int = 0
|
|
48
|
+
raw_output: str = ""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class LinterConfig:
|
|
52
|
+
"""Configuration for a specific linter"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
name: str,
|
|
57
|
+
command: List[str],
|
|
58
|
+
file_patterns: List[str],
|
|
59
|
+
parser: str = "default"
|
|
60
|
+
):
|
|
61
|
+
self.name = name
|
|
62
|
+
self.command = command
|
|
63
|
+
self.file_patterns = file_patterns
|
|
64
|
+
self.parser = parser
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# Built-in linter configurations
|
|
68
|
+
LINTERS = {
|
|
69
|
+
# Python
|
|
70
|
+
"ruff": LinterConfig(
|
|
71
|
+
name="ruff",
|
|
72
|
+
command=["ruff", "check", "--output-format=json"],
|
|
73
|
+
file_patterns=["*.py"],
|
|
74
|
+
parser="ruff"
|
|
75
|
+
),
|
|
76
|
+
"flake8": LinterConfig(
|
|
77
|
+
name="flake8",
|
|
78
|
+
command=["flake8", "--format=json"],
|
|
79
|
+
file_patterns=["*.py"],
|
|
80
|
+
parser="flake8"
|
|
81
|
+
),
|
|
82
|
+
"mypy": LinterConfig(
|
|
83
|
+
name="mypy",
|
|
84
|
+
command=["mypy", "--output=json"],
|
|
85
|
+
file_patterns=["*.py"],
|
|
86
|
+
parser="mypy"
|
|
87
|
+
),
|
|
88
|
+
"pylint": LinterConfig(
|
|
89
|
+
name="pylint",
|
|
90
|
+
command=["pylint", "--output-format=json"],
|
|
91
|
+
file_patterns=["*.py"],
|
|
92
|
+
parser="pylint"
|
|
93
|
+
),
|
|
94
|
+
|
|
95
|
+
# JavaScript/TypeScript
|
|
96
|
+
"eslint": LinterConfig(
|
|
97
|
+
name="eslint",
|
|
98
|
+
command=["eslint", "--format=json"],
|
|
99
|
+
file_patterns=["*.js", "*.jsx", "*.ts", "*.tsx"],
|
|
100
|
+
parser="eslint"
|
|
101
|
+
),
|
|
102
|
+
"tsc": LinterConfig(
|
|
103
|
+
name="tsc",
|
|
104
|
+
command=["tsc", "--noEmit"],
|
|
105
|
+
file_patterns=["*.ts", "*.tsx"],
|
|
106
|
+
parser="typescript"
|
|
107
|
+
),
|
|
108
|
+
|
|
109
|
+
# Go
|
|
110
|
+
"golint": LinterConfig(
|
|
111
|
+
name="golint",
|
|
112
|
+
command=["golangci-lint", "run", "--out-format=json"],
|
|
113
|
+
file_patterns=["*.go"],
|
|
114
|
+
parser="golangci"
|
|
115
|
+
),
|
|
116
|
+
|
|
117
|
+
# Rust
|
|
118
|
+
"clippy": LinterConfig(
|
|
119
|
+
name="clippy",
|
|
120
|
+
command=["cargo", "clippy", "--message-format=json"],
|
|
121
|
+
file_patterns=["*.rs"],
|
|
122
|
+
parser="cargo"
|
|
123
|
+
),
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class LintingManager:
|
|
128
|
+
"""
|
|
129
|
+
Manages linting operations.
|
|
130
|
+
|
|
131
|
+
Features:
|
|
132
|
+
- Auto-detect available linters
|
|
133
|
+
- Run linters on files/directories
|
|
134
|
+
- Parse and format output
|
|
135
|
+
- Support custom linter configs
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
def __init__(self, project_path: Optional[str] = None):
|
|
139
|
+
self.project_path = Path(project_path) if project_path else Path.cwd()
|
|
140
|
+
self._available_linters: Dict[str, LinterConfig] = {}
|
|
141
|
+
self._detect_linters()
|
|
142
|
+
|
|
143
|
+
def _detect_linters(self) -> None:
|
|
144
|
+
"""Detect available linters"""
|
|
145
|
+
for name, config in LINTERS.items():
|
|
146
|
+
if self._is_command_available(config.command[0]):
|
|
147
|
+
self._available_linters[name] = config
|
|
148
|
+
|
|
149
|
+
def _is_command_available(self, command: str) -> bool:
|
|
150
|
+
"""Check if a command is available"""
|
|
151
|
+
try:
|
|
152
|
+
result = subprocess.run(
|
|
153
|
+
["which", command],
|
|
154
|
+
capture_output=True,
|
|
155
|
+
timeout=5
|
|
156
|
+
)
|
|
157
|
+
return result.returncode == 0
|
|
158
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def available_linters(self) -> List[str]:
|
|
163
|
+
"""Get list of available linters"""
|
|
164
|
+
return list(self._available_linters.keys())
|
|
165
|
+
|
|
166
|
+
def get_linter_for_file(self, file_path: str) -> Optional[str]:
|
|
167
|
+
"""Get the best linter for a file type"""
|
|
168
|
+
path = Path(file_path)
|
|
169
|
+
ext = path.suffix.lower()
|
|
170
|
+
|
|
171
|
+
# Priority order for different languages
|
|
172
|
+
priority = {
|
|
173
|
+
".py": ["ruff", "flake8", "pylint"],
|
|
174
|
+
".js": ["eslint"],
|
|
175
|
+
".jsx": ["eslint"],
|
|
176
|
+
".ts": ["eslint", "tsc"],
|
|
177
|
+
".tsx": ["eslint", "tsc"],
|
|
178
|
+
".go": ["golint"],
|
|
179
|
+
".rs": ["clippy"],
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
linter_priority = priority.get(ext, [])
|
|
183
|
+
for linter in linter_priority:
|
|
184
|
+
if linter in self._available_linters:
|
|
185
|
+
return linter
|
|
186
|
+
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
def run_linter(
|
|
190
|
+
self,
|
|
191
|
+
linter_name: str,
|
|
192
|
+
target: Optional[str] = None,
|
|
193
|
+
fix: bool = False
|
|
194
|
+
) -> LintResult:
|
|
195
|
+
"""
|
|
196
|
+
Run a specific linter.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
linter_name: Name of the linter to run
|
|
200
|
+
target: File or directory to lint (defaults to project root)
|
|
201
|
+
fix: Whether to auto-fix issues (if supported)
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
LintResult with issues found
|
|
205
|
+
"""
|
|
206
|
+
if linter_name not in self._available_linters:
|
|
207
|
+
return LintResult(
|
|
208
|
+
success=False,
|
|
209
|
+
linter=linter_name,
|
|
210
|
+
raw_output=f"Linter '{linter_name}' not available"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
config = self._available_linters[linter_name]
|
|
214
|
+
cmd = config.command.copy()
|
|
215
|
+
|
|
216
|
+
# Add fix flag if supported
|
|
217
|
+
if fix:
|
|
218
|
+
if linter_name == "ruff":
|
|
219
|
+
cmd.append("--fix")
|
|
220
|
+
elif linter_name == "eslint":
|
|
221
|
+
cmd.append("--fix")
|
|
222
|
+
|
|
223
|
+
# Add target
|
|
224
|
+
if target:
|
|
225
|
+
cmd.append(target)
|
|
226
|
+
else:
|
|
227
|
+
cmd.append(str(self.project_path))
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
result = subprocess.run(
|
|
231
|
+
cmd,
|
|
232
|
+
cwd=str(self.project_path),
|
|
233
|
+
capture_output=True,
|
|
234
|
+
text=True,
|
|
235
|
+
timeout=120
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
issues = self._parse_output(config.parser, result.stdout, result.stderr)
|
|
239
|
+
|
|
240
|
+
error_count = sum(1 for i in issues if i.severity == LintSeverity.ERROR)
|
|
241
|
+
warning_count = sum(1 for i in issues if i.severity == LintSeverity.WARNING)
|
|
242
|
+
|
|
243
|
+
return LintResult(
|
|
244
|
+
success=result.returncode == 0,
|
|
245
|
+
linter=linter_name,
|
|
246
|
+
issues=issues,
|
|
247
|
+
error_count=error_count,
|
|
248
|
+
warning_count=warning_count,
|
|
249
|
+
raw_output=result.stdout or result.stderr
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
except subprocess.TimeoutExpired:
|
|
253
|
+
return LintResult(
|
|
254
|
+
success=False,
|
|
255
|
+
linter=linter_name,
|
|
256
|
+
raw_output="Linter timed out"
|
|
257
|
+
)
|
|
258
|
+
except Exception as e:
|
|
259
|
+
return LintResult(
|
|
260
|
+
success=False,
|
|
261
|
+
linter=linter_name,
|
|
262
|
+
raw_output=str(e)
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
def _parse_output(
|
|
266
|
+
self,
|
|
267
|
+
parser: str,
|
|
268
|
+
stdout: str,
|
|
269
|
+
stderr: str
|
|
270
|
+
) -> List[LintIssue]:
|
|
271
|
+
"""Parse linter output"""
|
|
272
|
+
issues = []
|
|
273
|
+
|
|
274
|
+
if parser == "ruff":
|
|
275
|
+
issues = self._parse_ruff(stdout)
|
|
276
|
+
elif parser == "eslint":
|
|
277
|
+
issues = self._parse_eslint(stdout)
|
|
278
|
+
elif parser == "flake8":
|
|
279
|
+
issues = self._parse_flake8(stdout)
|
|
280
|
+
elif parser == "pylint":
|
|
281
|
+
issues = self._parse_pylint(stdout)
|
|
282
|
+
elif parser == "typescript":
|
|
283
|
+
issues = self._parse_typescript(stdout, stderr)
|
|
284
|
+
elif parser == "golangci":
|
|
285
|
+
issues = self._parse_golangci(stdout)
|
|
286
|
+
else:
|
|
287
|
+
# Default: try to parse as generic output
|
|
288
|
+
issues = self._parse_generic(stdout, stderr)
|
|
289
|
+
|
|
290
|
+
return issues
|
|
291
|
+
|
|
292
|
+
def _parse_ruff(self, output: str) -> List[LintIssue]:
|
|
293
|
+
"""Parse ruff JSON output"""
|
|
294
|
+
issues = []
|
|
295
|
+
try:
|
|
296
|
+
data = json.loads(output) if output else []
|
|
297
|
+
for item in data:
|
|
298
|
+
issues.append(LintIssue(
|
|
299
|
+
file=item.get("filename", ""),
|
|
300
|
+
line=item.get("location", {}).get("row", 0),
|
|
301
|
+
column=item.get("location", {}).get("column", 0),
|
|
302
|
+
severity=LintSeverity.ERROR,
|
|
303
|
+
message=item.get("message", ""),
|
|
304
|
+
rule=item.get("code"),
|
|
305
|
+
source="ruff"
|
|
306
|
+
))
|
|
307
|
+
except json.JSONDecodeError:
|
|
308
|
+
pass
|
|
309
|
+
return issues
|
|
310
|
+
|
|
311
|
+
def _parse_eslint(self, output: str) -> List[LintIssue]:
|
|
312
|
+
"""Parse ESLint JSON output"""
|
|
313
|
+
issues = []
|
|
314
|
+
try:
|
|
315
|
+
data = json.loads(output) if output else []
|
|
316
|
+
for file_result in data:
|
|
317
|
+
file_path = file_result.get("filePath", "")
|
|
318
|
+
for msg in file_result.get("messages", []):
|
|
319
|
+
severity = LintSeverity.ERROR if msg.get("severity", 0) == 2 else LintSeverity.WARNING
|
|
320
|
+
issues.append(LintIssue(
|
|
321
|
+
file=file_path,
|
|
322
|
+
line=msg.get("line", 0),
|
|
323
|
+
column=msg.get("column", 0),
|
|
324
|
+
severity=severity,
|
|
325
|
+
message=msg.get("message", ""),
|
|
326
|
+
rule=msg.get("ruleId"),
|
|
327
|
+
source="eslint"
|
|
328
|
+
))
|
|
329
|
+
except json.JSONDecodeError:
|
|
330
|
+
pass
|
|
331
|
+
return issues
|
|
332
|
+
|
|
333
|
+
def _parse_flake8(self, output: str) -> List[LintIssue]:
|
|
334
|
+
"""Parse flake8 output"""
|
|
335
|
+
issues = []
|
|
336
|
+
# flake8 format: file:line:col: code message
|
|
337
|
+
for line in output.split('\n'):
|
|
338
|
+
if ':' in line:
|
|
339
|
+
parts = line.split(':', 3)
|
|
340
|
+
if len(parts) >= 4:
|
|
341
|
+
try:
|
|
342
|
+
issues.append(LintIssue(
|
|
343
|
+
file=parts[0],
|
|
344
|
+
line=int(parts[1]),
|
|
345
|
+
column=int(parts[2]),
|
|
346
|
+
severity=LintSeverity.ERROR,
|
|
347
|
+
message=parts[3].strip(),
|
|
348
|
+
source="flake8"
|
|
349
|
+
))
|
|
350
|
+
except (ValueError, IndexError):
|
|
351
|
+
pass
|
|
352
|
+
return issues
|
|
353
|
+
|
|
354
|
+
def _parse_pylint(self, output: str) -> List[LintIssue]:
|
|
355
|
+
"""Parse pylint JSON output"""
|
|
356
|
+
issues = []
|
|
357
|
+
try:
|
|
358
|
+
data = json.loads(output) if output else []
|
|
359
|
+
for item in data:
|
|
360
|
+
severity_map = {
|
|
361
|
+
"error": LintSeverity.ERROR,
|
|
362
|
+
"warning": LintSeverity.WARNING,
|
|
363
|
+
"convention": LintSeverity.INFO,
|
|
364
|
+
"refactor": LintSeverity.HINT,
|
|
365
|
+
}
|
|
366
|
+
issues.append(LintIssue(
|
|
367
|
+
file=item.get("path", ""),
|
|
368
|
+
line=item.get("line", 0),
|
|
369
|
+
column=item.get("column", 0),
|
|
370
|
+
severity=severity_map.get(item.get("type", "").lower(), LintSeverity.WARNING),
|
|
371
|
+
message=item.get("message", ""),
|
|
372
|
+
rule=item.get("symbol"),
|
|
373
|
+
source="pylint"
|
|
374
|
+
))
|
|
375
|
+
except json.JSONDecodeError:
|
|
376
|
+
pass
|
|
377
|
+
return issues
|
|
378
|
+
|
|
379
|
+
def _parse_typescript(self, stdout: str, stderr: str) -> List[LintIssue]:
|
|
380
|
+
"""Parse TypeScript compiler output"""
|
|
381
|
+
issues = []
|
|
382
|
+
output = stderr or stdout
|
|
383
|
+
# tsc format: file(line,col): error TS1234: message
|
|
384
|
+
for line in output.split('\n'):
|
|
385
|
+
if ': error ' in line or ': warning ' in line:
|
|
386
|
+
try:
|
|
387
|
+
# Parse the line
|
|
388
|
+
parts = line.split(':', 2)
|
|
389
|
+
if len(parts) >= 3:
|
|
390
|
+
file_loc = parts[0]
|
|
391
|
+
if '(' in file_loc:
|
|
392
|
+
file_path = file_loc[:file_loc.index('(')]
|
|
393
|
+
loc = file_loc[file_loc.index('(')+1:file_loc.index(')')]
|
|
394
|
+
line_num, col = loc.split(',')
|
|
395
|
+
severity = LintSeverity.ERROR if 'error' in parts[1].lower() else LintSeverity.WARNING
|
|
396
|
+
issues.append(LintIssue(
|
|
397
|
+
file=file_path,
|
|
398
|
+
line=int(line_num),
|
|
399
|
+
column=int(col),
|
|
400
|
+
severity=severity,
|
|
401
|
+
message=parts[2].strip(),
|
|
402
|
+
source="tsc"
|
|
403
|
+
))
|
|
404
|
+
except (ValueError, IndexError):
|
|
405
|
+
pass
|
|
406
|
+
return issues
|
|
407
|
+
|
|
408
|
+
def _parse_golangci(self, output: str) -> List[LintIssue]:
|
|
409
|
+
"""Parse golangci-lint JSON output"""
|
|
410
|
+
issues = []
|
|
411
|
+
try:
|
|
412
|
+
data = json.loads(output) if output else {}
|
|
413
|
+
for item in data.get("Issues", []):
|
|
414
|
+
issues.append(LintIssue(
|
|
415
|
+
file=item.get("Pos", {}).get("Filename", ""),
|
|
416
|
+
line=item.get("Pos", {}).get("Line", 0),
|
|
417
|
+
column=item.get("Pos", {}).get("Column", 0),
|
|
418
|
+
severity=LintSeverity.ERROR,
|
|
419
|
+
message=item.get("Text", ""),
|
|
420
|
+
rule=item.get("FromLinter"),
|
|
421
|
+
source="golangci-lint"
|
|
422
|
+
))
|
|
423
|
+
except json.JSONDecodeError:
|
|
424
|
+
pass
|
|
425
|
+
return issues
|
|
426
|
+
|
|
427
|
+
def _parse_generic(self, stdout: str, stderr: str) -> List[LintIssue]:
|
|
428
|
+
"""Generic parser for unknown output formats"""
|
|
429
|
+
issues = []
|
|
430
|
+
output = stderr or stdout
|
|
431
|
+
|
|
432
|
+
for line in output.split('\n'):
|
|
433
|
+
line = line.strip()
|
|
434
|
+
if not line or line.startswith('#'):
|
|
435
|
+
continue
|
|
436
|
+
|
|
437
|
+
# Try common patterns
|
|
438
|
+
if ':' in line:
|
|
439
|
+
issues.append(LintIssue(
|
|
440
|
+
file="",
|
|
441
|
+
line=0,
|
|
442
|
+
column=0,
|
|
443
|
+
severity=LintSeverity.WARNING,
|
|
444
|
+
message=line,
|
|
445
|
+
source="unknown"
|
|
446
|
+
))
|
|
447
|
+
|
|
448
|
+
return issues
|
|
449
|
+
|
|
450
|
+
def lint_file(self, file_path: str, fix: bool = False) -> Optional[LintResult]:
|
|
451
|
+
"""Lint a specific file"""
|
|
452
|
+
linter = self.get_linter_for_file(file_path)
|
|
453
|
+
if not linter:
|
|
454
|
+
return None
|
|
455
|
+
return self.run_linter(linter, file_path, fix=fix)
|
|
456
|
+
|
|
457
|
+
def lint_project(self, fix: bool = False) -> Dict[str, LintResult]:
|
|
458
|
+
"""Lint the entire project with all available linters"""
|
|
459
|
+
results = {}
|
|
460
|
+
for linter_name in self._available_linters:
|
|
461
|
+
results[linter_name] = self.run_linter(linter_name, fix=fix)
|
|
462
|
+
return results
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
# Global linting manager
|
|
466
|
+
_linting_manager: Optional[LintingManager] = None
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def get_linting_manager() -> LintingManager:
|
|
470
|
+
"""Get or create the global linting manager"""
|
|
471
|
+
global _linting_manager
|
|
472
|
+
if _linting_manager is None:
|
|
473
|
+
_linting_manager = LintingManager()
|
|
474
|
+
return _linting_manager
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def format_lint_result(result: LintResult) -> str:
|
|
478
|
+
"""Format a lint result for display"""
|
|
479
|
+
lines = []
|
|
480
|
+
|
|
481
|
+
if not result.issues:
|
|
482
|
+
lines.append(f"\033[32m{result.linter}: No issues found\033[0m")
|
|
483
|
+
return "\n".join(lines)
|
|
484
|
+
|
|
485
|
+
lines.append(f"\033[1m{result.linter}: {result.error_count} errors, {result.warning_count} warnings\033[0m\n")
|
|
486
|
+
|
|
487
|
+
for issue in result.issues[:20]: # Limit to 20 issues
|
|
488
|
+
severity_color = {
|
|
489
|
+
LintSeverity.ERROR: "\033[31m",
|
|
490
|
+
LintSeverity.WARNING: "\033[33m",
|
|
491
|
+
LintSeverity.INFO: "\033[36m",
|
|
492
|
+
LintSeverity.HINT: "\033[90m",
|
|
493
|
+
}.get(issue.severity, "")
|
|
494
|
+
|
|
495
|
+
file_loc = f"{issue.file}:{issue.line}:{issue.column}" if issue.file else ""
|
|
496
|
+
rule_str = f" [{issue.rule}]" if issue.rule else ""
|
|
497
|
+
|
|
498
|
+
lines.append(f" {severity_color}{issue.severity.value}{rule_str}\033[0m {file_loc}")
|
|
499
|
+
lines.append(f" {issue.message}")
|
|
500
|
+
|
|
501
|
+
if len(result.issues) > 20:
|
|
502
|
+
lines.append(f"\n ... and {len(result.issues) - 20} more issues")
|
|
503
|
+
|
|
504
|
+
return "\n".join(lines)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def generate_fix_prompt(result: LintResult) -> str:
|
|
508
|
+
"""Generate a prompt for AI to fix lint issues"""
|
|
509
|
+
issues_text = []
|
|
510
|
+
|
|
511
|
+
for issue in result.issues[:10]: # Limit for context
|
|
512
|
+
issues_text.append(f"- {issue.file}:{issue.line}: {issue.message}")
|
|
513
|
+
|
|
514
|
+
return f"""Fix the following {result.linter} issues:
|
|
515
|
+
|
|
516
|
+
{chr(10).join(issues_text)}
|
|
517
|
+
|
|
518
|
+
Please fix each issue while maintaining the existing code functionality.
|
|
519
|
+
"""
|