Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# CLAUDE.md - JEditor Project Guidelines

## Project Overview

JEditor is a Python-based code editor built with PySide6 (Qt), featuring syntax highlighting, code formatting, plugin system, Git integration, and LangChain-powered AI assistance.

- **Language**: Python 3.10+
- **UI Framework**: PySide6 6.11.0
- **Package Manager**: pip / setuptools
- **Testing**: pytest + pytest-qt
- **Linting**: ruff, pycodestyle
- **Formatting**: yapf

## Project Structure

```
je_editor/ # Main package
pyside_ui/ # UI layer (Qt widgets)
browser/ # Built-in browser
code/ # Code editor components (syntax, formatting, process management)
dialog/ # Dialog windows
git_ui/ # Git UI components
main_ui/ # Main window and layout
code_scan/ # Ruff linting & watchdog file monitoring
git_client/ # Git operations (GitPython)
plugins/ # Plugin loader system
utils/ # Shared utilities
test/ # Unit tests (pytest)
docs/ # Sphinx documentation
```

## Build & Test Commands

```bash
# Install dependencies
pip install -r requirements.txt

# Install dev dependencies
pip install -r dev_requirements.txt

# Run tests (excludes Qt UI tests by default)
pytest

# Build package (stable)
python -m build

# Build dev package (rename pyproject.toml <-> dev.toml first)
python -m build
```

## Design Principles

### Design Patterns

- **MVC separation**: UI widgets (View) in `pyside_ui/`, business logic (Model/Controller) in `code_scan/`, `git_client/`, `utils/`
- **Plugin architecture**: Extend functionality via `plugins/plugin_loader.py` without modifying core code
- **Observer pattern**: Use Qt signals/slots for decoupled event communication between components
- **Strategy pattern**: Code formatting and syntax highlighting are interchangeable strategies
- **Single Responsibility**: Each module handles one concern; avoid god classes

### Software Engineering

- **DRY**: Extract shared logic into `utils/`; never duplicate code across modules
- **SOLID principles**: Favor composition over inheritance; depend on abstractions
- **Fail fast**: Validate inputs at boundaries; raise clear exceptions early
- **Minimal public API**: Keep internal methods private (`_prefix`); expose only what consumers need
- **Type hints**: All function signatures must include type annotations

### Performance

- **Lazy loading**: Defer heavy imports and widget initialization until needed
- **Thread safety**: Run file I/O, Git operations, linting, and process execution in `QThread` or background threads — never block the UI thread
- **Efficient string handling**: Use `QTextCursor` batch operations for bulk text edits; avoid repeated `setText()` calls
- **Resource cleanup**: Always release file handles, threads, and subprocesses in `closeEvent` or `deleteLater`
- **Debounce**: Throttle expensive operations triggered by rapid user input (typing, resizing)

### Security (Mandatory)

- **No shell injection**: Never pass unsanitized user input to `subprocess.Popen(shell=True)` or `os.system()`. Use `subprocess.run()` with argument lists
- **Path traversal prevention**: Validate and sanitize all file paths from user input; reject paths containing `..` when operating within a project directory
- **No eval/exec on user data**: Never use `eval()`, `exec()`, or `compile()` on untrusted input
- **Sensitive data**: Never hardcode API keys, tokens, or credentials. Load from environment variables or config files excluded via `.gitignore`
- **Dependency awareness**: Pin dependency versions; review before upgrading
- **Input validation**: Sanitize all external input (file content, plugin data, user dialog input) at system boundaries

## Code Style

- Follow PEP 8; enforced by ruff
- Use `snake_case` for functions/variables, `PascalCase` for classes
- Keep functions short and focused (< 50 lines preferred)
- Remove dead code — do not comment out unused blocks or leave `# TODO` stubs without tracking

## Static Analysis Compliance (SonarQube / Codacy)

All code must pass SonarQube and Codacy quality gates. Adhere to the following rules:

### Complexity & Maintainability

- **Cognitive complexity**: Keep functions below 15 (SonarQube S3776). Break deeply nested logic into helper functions
- **Cyclomatic complexity**: Functions should stay under 10 branches; extract conditionals into smaller functions
- **Function length**: Soft limit 50 lines, hard limit 80 lines of executable code
- **Parameter count**: Max 7 parameters per function (SonarQube S107); use dataclasses or `**kwargs` for larger sets
- **Nesting depth**: Max 4 levels of nested control flow (SonarQube S134)
- **No duplicate code**: Extract 3+ line repeated blocks into shared utilities (SonarQube copy-paste detector)
- **String literal duplication**: Extract any string literal used 3+ times into a module-level constant (SonarQube S1192)
- **No magic numbers**: Replace unnamed numeric literals with named constants (SonarQube S109); exceptions: `0`, `1`, `-1`, `2`

### Exception Handling

- **No bare `except:`**: Always specify exception types (SonarQube S5754, Codacy PyLint W0702)
- **No silent swallowing**: Never `except: pass` without logging or re-raising (SonarQube S2737)
- **No overly broad `except Exception`** unless logged and re-raised at boundaries
- **Chain exceptions**: Use `raise NewError(...) from original_error` to preserve context
- **Use `logging` over `print`** for diagnostics in library/production code (SonarQube S4792)

### Code Quality

- **No commented-out code**: Delete it — rely on git history (SonarQube S125)
- **No unused imports/variables/parameters**: Remove them (SonarQube S1128, S1854)
- **Explicit `None` checks**: Use `is None` / `is not None`, never `== None` (SonarQube S2197)
- **No redundant boolean**: `if x:` not `if x == True:`; `if not x:` not `if x == False:`
- **Consistent return types**: A function should always return the same type (or always `None`); avoid `return None` in numeric functions
- **No assignment in conditions**: Avoid `if (x := func()):` in complex expressions (SonarQube S1121)
- **Identifier naming**: Min 3 characters except loop counters (`i`, `j`, `k`); no single-letter names for non-trivial scope
- **String formatting**: Prefer f-strings over `%` or `.format()` unless logging (logging uses `%` lazy formatting)

### Security (SonarQube / Codacy Bandit rules)

- **No hardcoded credentials** (SonarQube S2068, Bandit B105/B106): passwords, tokens, keys
- **No weak hashing** for security contexts (SonarQube S4790, Bandit B303): MD5/SHA1 only allowed for non-security uses (e.g., cache keys) with explicit comment
- **No `random` for security** (Bandit B311): use `secrets` module for tokens, IDs, crypto
- **No `pickle`/`marshal` on untrusted data** (Bandit B301/B302)
- **No `yaml.load` without `SafeLoader`** (Bandit B506)
- **No `tempfile.mktemp`** (Bandit B306): use `NamedTemporaryFile` / `mkstemp`
- **No `assert` in production logic** (Bandit B101): asserts are stripped with `-O`; use explicit `raise`
- **No XML parsers vulnerable to XXE** (Bandit B313-B320): use `defusedxml`
- **TLS verification**: Never `verify=False` in `requests` or urllib calls

### Resource Management

- **Always use context managers**: `with open(...)`, `with lock`, `with QMutexLocker(...)` — never manual `.close()` without `try/finally`
- **Close Qt resources**: call `deleteLater()` or use `setAttribute(Qt.WA_DeleteOnClose)` for modal dialogs
- **Encoding explicit**: always pass `encoding='utf-8'` to `open()` (SonarQube S5122 / Ruff PLW1514)

### Testing & Documentation

- **No empty test functions** (SonarQube S1186)
- **No identical test cases** (SonarQube S4144)
- **Public API docstrings**: all public classes/functions should have docstrings describing purpose, args, returns, raises

## Git & Commit Rules

- **Commit messages**: Write in English, concise, imperative mood (e.g., "Add plugin hot-reload support")
- **No AI attribution**: Do not mention any AI tool, assistant, or model name in commit messages or code comments
- **Branch strategy**: `main` = stable release, `dev` = active development
- **Clean commits**: Each commit should be a single logical change; no unrelated changes bundled together
4 changes: 2 additions & 2 deletions je_editor/code_scan/ruff_thread.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class RuffThread(threading.Thread):
使用子執行緒執行 Ruff (Python 程式碼檢查/格式化工具)。
"""

def __init__(self, ruff_commands: list, std_queue: Queue, stderr_queue: Queue):
def __init__(self, ruff_commands: list, std_queue: Queue, stderr_queue: Queue) -> None:
"""
Initialize the RuffThread.
初始化 RuffThread。
Expand All @@ -32,7 +32,7 @@ def __init__(self, ruff_commands: list, std_queue: Queue, stderr_queue: Queue):
self.std_queue = std_queue
self.stderr_queue = stderr_queue

def run(self):
def run(self) -> None:
"""
Run the Ruff process in a separate thread.
在子執行緒中執行 Ruff 程式。
Expand Down
8 changes: 4 additions & 4 deletions je_editor/code_scan/watchdog_implement.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from queue import Queue
from typing import Dict

from watchdog.events import FileSystemEventHandler
from watchdog.events import FileSystemEvent, FileSystemEventHandler

from je_editor.code_scan.ruff_thread import RuffThread

Expand All @@ -13,7 +13,7 @@ class RuffPythonFileChangeHandler(FileSystemEventHandler):
當 Python 檔案被修改時,自動觸發 Ruff 檢查。
"""

def __init__(self, ruff_commands: list = None, debounce_interval: float = 1.0):
def __init__(self, ruff_commands: list = None, debounce_interval: float = 1.0) -> None:
"""
:param ruff_commands: Ruff command list, e.g. ["ruff", "check"]
:param debounce_interval: Minimum interval (seconds) between re-runs for the same file
Expand All @@ -27,14 +27,14 @@ def __init__(self, ruff_commands: list = None, debounce_interval: float = 1.0):
self.last_run_time: Dict[str, float] = {}
self.debounce_interval = debounce_interval

def _start_new_thread(self, file_path: str):
def _start_new_thread(self, file_path: str) -> None:
"""Helper to start a new Ruff thread for a given file."""
ruff_thread = RuffThread(self.ruff_commands, self.stdout_queue, self.stderr_queue)
self.ruff_threads_dict[file_path] = ruff_thread
self.last_run_time[file_path] = time.time()
ruff_thread.start()

def on_modified(self, event):
def on_modified(self, event: FileSystemEvent) -> None:
"""Triggered when a file is modified."""
if event.is_directory:
return
Expand Down
24 changes: 12 additions & 12 deletions je_editor/code_scan/watchdog_thread.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import sys
import threading
import time
from pathlib import Path

from watchdog.observers import Observer

from je_editor.code_scan.watchdog_implement import RuffPythonFileChangeHandler
from je_editor.utils.logging.loggin_instance import jeditor_logger


class WatchdogThread(threading.Thread):
Expand All @@ -14,7 +14,7 @@ class WatchdogThread(threading.Thread):
使用 watchdog 監控檔案變化的執行緒。
"""

def __init__(self, check_path: str):
def __init__(self, check_path: str) -> None:
"""
:param check_path: Path to monitor (directory or file)
要監控的路徑(資料夾或檔案)
Expand All @@ -25,42 +25,42 @@ def __init__(self, check_path: str):
self.running = True
self.observer = Observer()

def run(self):
def run(self) -> None:
"""Start the watchdog observer loop."""
if not self.check_path.exists():
print(f"[Error] Path does not exist: {self.check_path}", file=sys.stderr)
jeditor_logger.error(f"[Watchdog] Path does not exist: {self.check_path}")
return

# 設定監控
self.observer.schedule(self.ruff_handler, str(self.check_path), recursive=True)
self.observer.start()
print(f"[Watchdog] Monitoring started on {self.check_path}")
jeditor_logger.info(f"[Watchdog] Monitoring started on {self.check_path}")

try:
while self.running:
time.sleep(1)
# 這裡可以加上 queue 輸出處理
self._process_ruff_output()
except KeyboardInterrupt:
print("[Watchdog] Interrupted by user")
jeditor_logger.info("[Watchdog] Interrupted by user")
finally:
self.observer.stop()
self.observer.join()
print("[Watchdog] Monitoring stopped")
jeditor_logger.info("[Watchdog] Monitoring stopped")

def stop(self):
def stop(self) -> None:
"""Stop the watchdog thread safely."""
self.running = False

def _process_ruff_output(self):
def _process_ruff_output(self) -> None:
"""Process stdout/stderr queues from Ruff threads."""
while not self.ruff_handler.stdout_queue.empty():
line = self.ruff_handler.stdout_queue.get()
print(f"[Ruff STDOUT] {line}")
jeditor_logger.info(f"[Ruff STDOUT] {line}")

while not self.ruff_handler.stderr_queue.empty():
line = self.ruff_handler.stderr_queue.get()
print(f"[Ruff STDERR] {line}", file=sys.stderr)
jeditor_logger.error(f"[Ruff STDERR] {line}")


if __name__ == '__main__':
Expand All @@ -73,6 +73,6 @@ def _process_ruff_output(self):
while True:
time.sleep(1)
except KeyboardInterrupt:
print("[Main] Stopping watchdog...")
jeditor_logger.info("[Main] Stopping watchdog...")
watchdog_thread.stop()
watchdog_thread.join()
Loading
Loading