invar-tools 1.11.0__py3-none-any.whl → 1.14.0__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.
@@ -7,6 +7,7 @@ Shell module: handles file I/O for map and sig commands.
7
7
  from __future__ import annotations
8
8
 
9
9
  import json
10
+ from dataclasses import dataclass
10
11
  from pathlib import Path
11
12
  from typing import TYPE_CHECKING
12
13
 
@@ -70,7 +71,11 @@ def run_sig(target: str, json_output: bool) -> Result[None, str]:
70
71
 
71
72
  file_path = Path(file_path_str)
72
73
  if not file_path.exists():
73
- return Failure(f"File not found: {file_path}")
74
+ # DX-78 Phase B: Suggest alternative tools
75
+ return Failure(
76
+ f"File not found: {file_path}\n\n"
77
+ "💡 Try using Grep to search for the symbol across the codebase."
78
+ )
74
79
 
75
80
  # Read file content
76
81
  try:
@@ -122,7 +127,59 @@ def _run_sig_python(
122
127
  def _run_sig_typescript(
123
128
  content: str, file_path: Path, symbol_name: str | None, json_output: bool
124
129
  ) -> Result[None, str]:
125
- """Run sig for TypeScript files (LX-06)."""
130
+ """Run sig for TypeScript files.
131
+
132
+ DX-78: Uses TS Compiler API when available, falls back to regex parser.
133
+ """
134
+ from invar.shell.ts_compiler import is_typescript_available, run_sig_typescript
135
+
136
+ # Try TS Compiler API first (DX-78)
137
+ if is_typescript_available():
138
+ sig_result = run_sig_typescript(file_path)
139
+ if isinstance(sig_result, Success):
140
+ symbols = sig_result.unwrap()
141
+
142
+ # Filter by symbol name if specified
143
+ if symbol_name:
144
+ symbols = [s for s in symbols if s.name == symbol_name]
145
+ if not symbols:
146
+ return Failure(f"Symbol '{symbol_name}' not found in {file_path}")
147
+
148
+ # Output using TS Compiler API format
149
+ if json_output:
150
+ output = {
151
+ "file": str(file_path),
152
+ "symbols": [
153
+ {
154
+ "name": s.name,
155
+ "kind": s.kind,
156
+ "signature": s.signature,
157
+ "line": s.line,
158
+ "contracts": s.contracts,
159
+ "members": s.members,
160
+ }
161
+ for s in symbols
162
+ ],
163
+ }
164
+ console.print(json.dumps(output, indent=2))
165
+ else:
166
+ console.print(f"[bold]{file_path}[/bold]")
167
+ for s in symbols:
168
+ console.print(f" [{s.kind}] {s.name}")
169
+ console.print(f" {s.signature}")
170
+ if s.contracts:
171
+ for pre in s.contracts.get("pre", []):
172
+ console.print(f" @pre {pre}")
173
+ for post in s.contracts.get("post", []):
174
+ console.print(f" @post {post}")
175
+ if s.members:
176
+ for m in s.members:
177
+ console.print(f" [{m['kind']}] {m['name']}: {m.get('signature', '')}")
178
+ console.print()
179
+
180
+ return Success(None)
181
+
182
+ # Fallback to regex parser (LX-06 legacy)
126
183
  from invar.core.ts_sig_parser import (
127
184
  extract_ts_signatures,
128
185
  format_ts_signatures_json,
@@ -172,7 +229,14 @@ def _run_map_python(path: Path, top_n: int, json_output: bool) -> Result[None, s
172
229
  continue
173
230
 
174
231
  if not file_infos:
175
- return Failure("No Python files found")
232
+ return Failure(
233
+ "No Python symbols found.\n\n"
234
+ "Available tools:\n"
235
+ "- invar sig <file.py> — Extract signatures\n"
236
+ "- invar refs <file.py>::Symbol — Find references\n"
237
+ "- invar_doc_* — Document navigation\n"
238
+ "- invar_guard — Static verification"
239
+ )
176
240
 
177
241
  # Build perception map
178
242
  perception_map = build_perception_map(file_infos, sources, str(path.absolute()))
@@ -190,10 +254,47 @@ def _run_map_python(path: Path, top_n: int, json_output: bool) -> Result[None, s
190
254
 
191
255
  # @shell_complexity: TypeScript map with file discovery and symbol extraction
192
256
  def _run_map_typescript(path: Path, top_n: int, json_output: bool) -> Result[None, str]:
193
- """Run map for TypeScript projects (LX-06).
257
+ """Run map for TypeScript projects.
194
258
 
195
- MVP: Lists symbols without reference counting (Phase 2 can add references).
259
+ DX-78: Uses TS Compiler API when available, falls back to regex parser.
196
260
  """
261
+ from invar.shell.ts_compiler import is_typescript_available, run_map_typescript
262
+
263
+ # Try TS Compiler API first (DX-78)
264
+ if is_typescript_available():
265
+ map_result = run_map_typescript(path, top_n)
266
+ if isinstance(map_result, Success):
267
+ data = map_result.unwrap()
268
+
269
+ if not data.get("symbols"):
270
+ return Failure(
271
+ "No TypeScript symbols found.\n\n"
272
+ "Available tools:\n"
273
+ "- invar sig <file.ts> — Extract signatures\n"
274
+ "- invar refs <file.ts>::Symbol — Find references\n"
275
+ "- invar_doc_* — Document navigation\n"
276
+ "- invar_guard — Static verification"
277
+ )
278
+
279
+ # Output using TS Compiler API format
280
+ if json_output:
281
+ output = {
282
+ "language": "typescript",
283
+ "total_symbols": data.get("total", len(data["symbols"])),
284
+ "symbols": data["symbols"],
285
+ }
286
+ console.print(json.dumps(output, indent=2))
287
+ else:
288
+ console.print("[bold]TypeScript Symbol Map[/bold]")
289
+ console.print(f"Total symbols: {data.get('total', len(data['symbols']))}\n")
290
+ for sym in data["symbols"]:
291
+ console.print(f"[{sym['kind']}] {sym['name']}")
292
+ console.print(f" {sym['file']}:{sym['line']}")
293
+ console.print()
294
+
295
+ return Success(None)
296
+
297
+ # Fallback to regex parser (LX-06 legacy)
197
298
  from invar.core.ts_sig_parser import TSSymbol, extract_ts_signatures
198
299
  from invar.shell.fs import discover_typescript_files
199
300
 
@@ -213,7 +314,14 @@ def _run_map_typescript(path: Path, top_n: int, json_output: bool) -> Result[Non
213
314
  continue
214
315
 
215
316
  if not all_symbols:
216
- return Failure("No TypeScript symbols found (files may be empty or contain no exportable symbols)")
317
+ return Failure(
318
+ "No TypeScript symbols found.\n\n"
319
+ "Available tools:\n"
320
+ "- invar sig <file.ts> — Extract signatures\n"
321
+ "- invar refs <file.ts>::Symbol — Find references\n"
322
+ "- invar_doc_* — Document navigation\n"
323
+ "- invar_guard — Static verification"
324
+ )
217
325
 
218
326
  # Sort by kind priority (function/class first), then by name
219
327
  kind_order = {"function": 0, "class": 1, "interface": 2, "type": 3, "const": 4, "method": 5}
@@ -249,3 +357,191 @@ def _run_map_typescript(path: Path, top_n: int, json_output: bool) -> Result[Non
249
357
  console.print()
250
358
 
251
359
  return Success(None)
360
+
361
+
362
+ # @shell_complexity: Reference finding with multi-language support and output formatting
363
+ def run_refs(target: str, json_output: bool) -> Result[None, str]:
364
+ """Find all references to a symbol.
365
+
366
+ Target format: "path/to/file.py::symbol_name" or "path/to/file.ts::symbol_name"
367
+ DX-78: Supports both Python (via jedi) and TypeScript (via TS Compiler API).
368
+ """
369
+ # Parse target
370
+ if "::" not in target:
371
+ return Failure(
372
+ "Invalid target format.\n\n"
373
+ "Expected: path/to/file.py::symbol_name\n"
374
+ "Example: src/auth.py::validate_token"
375
+ )
376
+
377
+ file_part, symbol_name = target.rsplit("::", 1)
378
+ file_path = Path(file_part)
379
+
380
+ if not file_path.exists():
381
+ return Failure(f"File not found: {file_path}")
382
+
383
+ suffix = file_path.suffix.lower()
384
+
385
+ # Route to language-specific implementation
386
+ if suffix in (".ts", ".tsx"):
387
+ return _run_refs_typescript(file_path, symbol_name, json_output)
388
+ elif suffix in (".py", ".pyi"):
389
+ return _run_refs_python(file_path, symbol_name, json_output)
390
+ else:
391
+ return Failure(
392
+ f"Unsupported file type: {suffix}\n\n"
393
+ "Supported: .py, .pyi, .ts, .tsx"
394
+ )
395
+
396
+
397
+ # @shell_complexity: Reference finding with output formatting and error handling
398
+ def _run_refs_python(
399
+ file_path: Path, symbol_name: str, json_output: bool
400
+ ) -> Result[None, str]:
401
+ """Find references in Python using jedi."""
402
+ from invar.shell.py_refs import find_all_references_to_symbol
403
+
404
+ # Find project root
405
+ project_root = file_path.parent
406
+ for parent in file_path.parents:
407
+ if (parent / "pyproject.toml").exists() or (parent / "setup.py").exists():
408
+ project_root = parent
409
+ break
410
+
411
+ refs = find_all_references_to_symbol(file_path, symbol_name, project_root)
412
+
413
+ if not refs:
414
+ return Failure(f"Symbol '{symbol_name}' not found in {file_path}")
415
+
416
+ # Output
417
+ if json_output:
418
+ output = {
419
+ "target": str(file_path) + "::" + symbol_name,
420
+ "total": len(refs),
421
+ "references": [
422
+ {
423
+ "file": str(ref.file.relative_to(project_root))
424
+ if ref.file.is_relative_to(project_root)
425
+ else str(ref.file),
426
+ "line": ref.line,
427
+ "column": ref.column,
428
+ "context": ref.context,
429
+ "is_definition": ref.is_definition,
430
+ }
431
+ for ref in refs
432
+ ],
433
+ }
434
+ console.print(json.dumps(output, indent=2))
435
+ else:
436
+ console.print(f"[bold]References to {symbol_name}[/bold]")
437
+ console.print(f"Found {len(refs)} reference(s)\n")
438
+
439
+ for ref in refs:
440
+ rel_path = (
441
+ ref.file.relative_to(project_root)
442
+ if ref.file.is_relative_to(project_root)
443
+ else ref.file
444
+ )
445
+ marker = " [definition]" if ref.is_definition else ""
446
+ console.print(f"{rel_path}:{ref.line}{marker}")
447
+ if ref.context:
448
+ console.print(f" {ref.context}")
449
+ console.print()
450
+
451
+ return Success(None)
452
+
453
+
454
+ @dataclass
455
+ class _SymbolPosition:
456
+ """Temporary holder for symbol position during refs lookup."""
457
+ line: int
458
+ column: int
459
+ name: str
460
+
461
+
462
+ # @shell_complexity: TypeScript refs with symbol lookup and output formatting
463
+ def _run_refs_typescript(
464
+ file_path: Path, symbol_name: str, json_output: bool
465
+ ) -> Result[None, str]:
466
+ """Find references in TypeScript using TS Compiler API."""
467
+ from invar.shell.ts_compiler import is_typescript_available, run_refs_typescript
468
+
469
+ if not is_typescript_available():
470
+ return Failure(
471
+ "TypeScript tools not available.\n\n"
472
+ "Requirements:\n"
473
+ "- Node.js installed\n"
474
+ "- tsconfig.json in project root"
475
+ )
476
+
477
+ # First, find the symbol's position using sig command
478
+ from invar.shell.ts_compiler import run_sig_typescript
479
+
480
+ sig_result = run_sig_typescript(file_path)
481
+ if isinstance(sig_result, Failure):
482
+ return sig_result
483
+
484
+ symbols = sig_result.unwrap()
485
+ symbol = next((s for s in symbols if s.name == symbol_name), None)
486
+
487
+ if symbol is None:
488
+ # Check class members
489
+ for s in symbols:
490
+ if s.members:
491
+ for member in s.members:
492
+ if member.get("name") == symbol_name:
493
+ # Extract column if available, default to 0
494
+ column = member.get("column", 0)
495
+ symbol = _SymbolPosition(
496
+ line=member["line"],
497
+ column=column,
498
+ name=symbol_name
499
+ )
500
+ break
501
+ if symbol:
502
+ break
503
+
504
+ if symbol is None:
505
+ return Failure(f"Symbol '{symbol_name}' not found in {file_path}")
506
+
507
+ # Find references using position
508
+ # Use symbol.column if available (from member dict), defaults to 0
509
+ column = getattr(symbol, "column", 0)
510
+ refs_result = run_refs_typescript(file_path, symbol.line, column)
511
+ if isinstance(refs_result, Failure):
512
+ return refs_result
513
+
514
+ refs = refs_result.unwrap()
515
+
516
+ if not refs:
517
+ return Failure(f"No references found for '{symbol_name}'")
518
+
519
+ # Output (refs already have relative paths from ts-query.js)
520
+ if json_output:
521
+ output = {
522
+ "target": str(file_path) + "::" + symbol_name,
523
+ "total": len(refs),
524
+ "references": [
525
+ {
526
+ "file": ref.file,
527
+ "line": ref.line,
528
+ "column": ref.column,
529
+ "context": ref.context,
530
+ "is_definition": ref.is_definition,
531
+ }
532
+ for ref in refs
533
+ ],
534
+ }
535
+ console.print(json.dumps(output, indent=2))
536
+ else:
537
+ console.print(f"[bold]References to {symbol_name}[/bold]")
538
+ console.print(f"Found {len(refs)} reference(s)\n")
539
+
540
+ for ref in refs:
541
+ marker = " [definition]" if ref.is_definition else ""
542
+ console.print(f"{ref.file}:{ref.line}{marker}")
543
+ if ref.context:
544
+ console.print(f" {ref.context}")
545
+ console.print()
546
+
547
+ return Success(None)
invar/shell/py_refs.py ADDED
@@ -0,0 +1,156 @@
1
+ """Python reference finding using jedi.
2
+
3
+ DX-78: Provides cross-file reference finding for Python symbols.
4
+ Shell module: Uses jedi library for I/O-based symbol analysis.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+
12
+ import jedi
13
+
14
+
15
+ @dataclass
16
+ class Reference:
17
+ """A reference to a Python symbol."""
18
+
19
+ file: Path
20
+ line: int
21
+ column: int
22
+ context: str
23
+ is_definition: bool = False
24
+
25
+
26
+ # @shell_complexity: Reference finding with jedi library and error handling
27
+ def find_references(
28
+ file_path: Path,
29
+ line: int,
30
+ column: int,
31
+ project_root: Path | None = None,
32
+ ) -> list[Reference]:
33
+ """Find all references to symbol at position using jedi.
34
+
35
+ Args:
36
+ file_path: File containing the symbol
37
+ line: 1-based line number
38
+ column: 0-based column number
39
+ project_root: Project root for cross-file resolution
40
+
41
+ Returns:
42
+ List of references found
43
+
44
+ >>> from pathlib import Path
45
+ >>> import tempfile, os
46
+ >>> # Test with a simple Python file
47
+ >>> with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
48
+ ... _ = f.write('def hello():\\n pass\\n')
49
+ ... temp_file = Path(f.name)
50
+ >>> # Find references returns a list (may be empty if jedi not configured)
51
+ >>> refs = find_references(temp_file, 1, 4)
52
+ >>> isinstance(refs, list)
53
+ True
54
+ >>> os.unlink(temp_file)
55
+ """
56
+ source = file_path.read_text(encoding="utf-8")
57
+
58
+ project = None
59
+ if project_root:
60
+ project = jedi.Project(path=str(project_root))
61
+
62
+ script = jedi.Script(source, path=str(file_path), project=project)
63
+ refs = script.get_references(line, column)
64
+
65
+ results: list[Reference] = []
66
+ for ref in refs:
67
+ # Skip builtins (no module_path)
68
+ if not ref.module_path:
69
+ continue
70
+
71
+ line_code = ref.get_line_code()
72
+ context = line_code.strip() if line_code else ""
73
+
74
+ results.append(
75
+ Reference(
76
+ file=Path(ref.module_path),
77
+ line=ref.line,
78
+ column=ref.column,
79
+ context=context,
80
+ is_definition=ref.is_definition(),
81
+ )
82
+ )
83
+
84
+ return results
85
+
86
+
87
+ # @shell_complexity: Symbol search using jedi library
88
+ def find_symbol_position(file_path: Path, symbol_name: str) -> tuple[int, int] | None:
89
+ """Find the position of a symbol definition in a file.
90
+
91
+ Args:
92
+ file_path: File to search
93
+ symbol_name: Name of the symbol to find
94
+
95
+ Returns:
96
+ Tuple of (line, column) or None if not found
97
+
98
+ >>> from pathlib import Path
99
+ >>> import tempfile, os
100
+ >>> # Test finding a function definition
101
+ >>> with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
102
+ ... _ = f.write('def test_func():\\n return 42\\n')
103
+ ... temp_file = Path(f.name)
104
+ >>> pos = find_symbol_position(temp_file, "test_func")
105
+ >>> isinstance(pos, tuple) or pos is None # Returns tuple or None
106
+ True
107
+ >>> os.unlink(temp_file)
108
+ """
109
+ source = file_path.read_text(encoding="utf-8")
110
+ script = jedi.Script(source, path=str(file_path))
111
+
112
+ # Get all names defined in the file
113
+ names = script.get_names(all_scopes=True)
114
+
115
+ for name in names:
116
+ if name.name == symbol_name and name.is_definition():
117
+ return (name.line, name.column)
118
+
119
+ return None
120
+
121
+
122
+ # @shell_complexity: Combines symbol position lookup and reference finding
123
+ def find_all_references_to_symbol(
124
+ file_path: Path,
125
+ symbol_name: str,
126
+ project_root: Path | None = None,
127
+ ) -> list[Reference]:
128
+ """Find all references to a named symbol.
129
+
130
+ Convenience function that combines find_symbol_position and find_references.
131
+
132
+ Args:
133
+ file_path: File containing the symbol definition
134
+ symbol_name: Name of the symbol
135
+ project_root: Project root for cross-file resolution
136
+
137
+ Returns:
138
+ List of references found
139
+
140
+ >>> from pathlib import Path
141
+ >>> import tempfile, os
142
+ >>> # Test finding all references to a symbol
143
+ >>> with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
144
+ ... _ = f.write('def greet():\\n pass\\ngreet()\\n')
145
+ ... temp_file = Path(f.name)
146
+ >>> refs = find_all_references_to_symbol(temp_file, "greet")
147
+ >>> isinstance(refs, list) # Returns list of references
148
+ True
149
+ >>> os.unlink(temp_file)
150
+ """
151
+ position = find_symbol_position(file_path, symbol_name)
152
+ if position is None:
153
+ return []
154
+
155
+ line, column = position
156
+ return find_references(file_path, line, column, project_root)