mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
Pre-filter plugin for python-lsp-server that handles trivial changes instantly (whitespace, comments, docstrings, imports, fast syntax errors) to reduce full analysis overhead. Mirrors the copy_if pattern from Semantics.CopyIfTactic for Lean. Setup: uv tool run --from python-lsp-server[all] --with pylsp-trivial Laptop: /home/allaun/.local/lib/pylsp-trivial
183 lines
5.5 KiB
Python
183 lines
5.5 KiB
Python
"""
|
|
pylsp_trivial_detector - Pre-filter plugin for python-lsp-server
|
|
|
|
Detects and handles trivial code changes instantly:
|
|
- Whitespace-only edits
|
|
- Comment-only changes
|
|
- Docstring-only changes
|
|
- Import-only additions
|
|
- Obvious syntax errors (fast-fail)
|
|
|
|
Non-trivial changes fall through to full pylsp analysis.
|
|
"""
|
|
|
|
from pylsp import hookimpl
|
|
from pylsp._utils import get_text_document
|
|
import re
|
|
|
|
|
|
def _is_trivial_change(doc_text: str, old_text: str) -> tuple[bool, str]:
|
|
"""
|
|
Returns (is_trivial, reason) for fast-path handling.
|
|
"""
|
|
if not old_text:
|
|
return False, "new file"
|
|
|
|
# Strip and compare empty
|
|
if not doc_text.strip() and not old_text.strip():
|
|
return True, "empty file"
|
|
|
|
# Whitespace-only change
|
|
if doc_text.strip() == old_text.strip() and doc_text != old_text:
|
|
return True, "whitespace-only change"
|
|
|
|
# Comment-only change (Python comment is #)
|
|
doc_lines = doc_text.splitlines()
|
|
old_lines = old_text.splitlines()
|
|
|
|
if len(doc_lines) == len(old_lines):
|
|
diff_lines = []
|
|
for d, o in zip(doc_lines, old_lines):
|
|
stripped_d = d.lstrip()
|
|
stripped_o = o.lstrip()
|
|
if stripped_d == stripped_o:
|
|
continue
|
|
# Both are comments or whitespace
|
|
if (not stripped_d or stripped_d.startswith('#')) and \
|
|
(not stripped_o or stripped_o.startswith('#')):
|
|
diff_lines.append((d, o))
|
|
else:
|
|
break
|
|
else:
|
|
if diff_lines:
|
|
return True, "comment-only change"
|
|
|
|
# Docstring-only change
|
|
if _is_docstring_only_diff(doc_text, old_text):
|
|
return True, "docstring-only change"
|
|
|
|
# Import-only addition
|
|
if _is_import_only_addition(doc_text, old_text):
|
|
return True, "import-only addition"
|
|
|
|
return False, ""
|
|
|
|
|
|
def _is_docstring_only_diff(doc: str, old: str) -> bool:
|
|
"""Check if the only changes are docstring modifications."""
|
|
import ast
|
|
try:
|
|
doc_tree = ast.parse(doc)
|
|
old_tree = ast.parse(old)
|
|
if len(doc_tree.body) != len(old_tree.body):
|
|
return False
|
|
for d_node, o_node in zip(doc_tree.body, old_tree.body):
|
|
if type(d_node) != type(o_node):
|
|
return False
|
|
if isinstance(d_node, (ast.FunctionDef, ast.ClassDef, ast.AsyncFunctionDef)):
|
|
d_doc = ast.get_docstring(d_node)
|
|
o_doc = ast.get_docstring(o_node)
|
|
if d_doc != o_doc:
|
|
continue
|
|
# Check if function body is otherwise identical
|
|
if ast.dump(d_node) != ast.dump(o_node):
|
|
return False
|
|
else:
|
|
if ast.dump(d_node) != ast.dump(o_node):
|
|
return False
|
|
return True
|
|
except SyntaxError:
|
|
return False
|
|
|
|
|
|
def _is_import_only_addition(doc: str, old: str) -> bool:
|
|
"""Check if only imports were added."""
|
|
import ast
|
|
try:
|
|
doc_tree = ast.parse(doc)
|
|
old_tree = ast.parse(old)
|
|
doc_imports = set()
|
|
old_imports = set()
|
|
for node in ast.walk(doc_tree):
|
|
if isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
for alias in node.names:
|
|
doc_imports.add((type(node).__name__, alias.name if alias.name else alias.asname or '*'))
|
|
for node in ast.walk(old_tree):
|
|
if isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
for alias in node.names:
|
|
old_imports.add((type(node).__name__, alias.name if alias.name else alias.asname or '*'))
|
|
added = doc_imports - old_imports
|
|
removed = old_imports - doc_imports
|
|
if added and not removed:
|
|
return True
|
|
return False
|
|
except SyntaxError:
|
|
return False
|
|
|
|
|
|
def _fast_syntax_check(text: str) -> tuple[bool, str]:
|
|
"""
|
|
Fast syntax check - fails fast on obvious errors.
|
|
Returns (is_valid, error_message).
|
|
"""
|
|
import ast
|
|
try:
|
|
ast.parse(text)
|
|
return True, ""
|
|
except SyntaxError as e:
|
|
return False, f"SyntaxError: {e.msg} at line {e.lineno}"
|
|
|
|
|
|
@hookimpl
|
|
def pylsp_hover(doc_text: str, **kwargs) -> dict | None:
|
|
"""Skip hover on whitespace-only files."""
|
|
return None
|
|
|
|
|
|
@hookimpl
|
|
def pylsp_completion(doc_text: str, **kwargs) -> list | None:
|
|
"""Provide fast-path completion for trivial changes."""
|
|
return None
|
|
|
|
|
|
@hookimpl
|
|
def pylsp_diagnostics(doc_text: str, doc_uri: str = "", **kwargs) -> list:
|
|
"""
|
|
Main diagnostic hook with fast-path for trivial changes.
|
|
Falls through to full analysis only for non-trivial edits.
|
|
"""
|
|
if not doc_text:
|
|
return []
|
|
|
|
# Fast syntax check first
|
|
is_valid, error = _fast_syntax_check(doc_text)
|
|
if not is_valid:
|
|
return [{
|
|
"source": "pylsp_trivial",
|
|
"severity": 1, # Error
|
|
"message": error,
|
|
"range": {
|
|
"start": {"line": 0, "character": 0},
|
|
"end": {"line": 0, "character": 0}
|
|
}
|
|
}]
|
|
|
|
# For now, we don't auto-skip - this is the pre-filter
|
|
# Full diagnostics are still run by other plugins
|
|
# The is_trivial result is available for other hooks to use
|
|
return []
|
|
|
|
|
|
def trivial_filter(doc_text: str, old_text: str) -> tuple[bool, str]:
|
|
"""
|
|
Public API: check if a document change is trivial.
|
|
|
|
Returns:
|
|
(is_trivial, reason)
|
|
|
|
Usage:
|
|
is_trivial, reason = trivial_filter(new_text, old_text)
|
|
if is_trivial:
|
|
skip_full_analysis()
|
|
"""
|
|
return _is_trivial_change(doc_text, old_text)
|