#!/usr/bin/env python3 """Locate likely RSF backend files for a feature/module keyword. Usage: python skills/rsf-server-maintainer/scripts/locate_module.py basStation python skills/rsf-server-maintainer/scripts/locate_module.py order --repo C:/env/code/wms-master/rsf-server """ from __future__ import annotations import argparse import re from pathlib import Path from typing import Iterable, List, Tuple def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Locate related controller/service/mapper/entity/XML files by keyword.") parser.add_argument("keyword", help="Module keyword, for example: basStation, order, user") parser.add_argument( "--repo", default=str(Path(__file__).resolve().parents[3]), help="Path to rsf-server repository root (default: auto-detect from script location)", ) parser.add_argument( "--limit", type=int, default=80, help="Max results per section (default: 80)", ) return parser.parse_args() def normalize_keyword(value: str) -> str: return value.strip().lower() def collect_files(root: Path, patterns: Iterable[str]) -> List[Path]: files: List[Path] = [] for pattern in patterns: files.extend(root.glob(pattern)) return [p for p in files if p.is_file()] def find_name_matches(files: Iterable[Path], keyword: str) -> List[Path]: matches = [] for file in files: text = str(file).lower() if keyword in text: matches.append(file) return sorted(set(matches)) def find_line_matches(files: Iterable[Path], keyword: str, pattern: re.Pattern[str]) -> List[Tuple[Path, int, str]]: out: List[Tuple[Path, int, str]] = [] for file in files: try: content = file.read_text(encoding="utf-8", errors="ignore").splitlines() except OSError: continue for index, line in enumerate(content, start=1): lower = line.lower() if keyword in lower and pattern.search(line): out.append((file, index, line.strip())) return out def print_file_section(title: str, repo: Path, files: List[Path], limit: int) -> None: print(f"\n[{title}] {len(files)}") for path in files[:limit]: rel = path.relative_to(repo) print(f" - {rel.as_posix()}") if len(files) > limit: print(f" ... ({len(files) - limit} more)") def print_line_section(title: str, repo: Path, rows: List[Tuple[Path, int, str]], limit: int) -> None: print(f"\n[{title}] {len(rows)}") for file, line_no, line in rows[:limit]: rel = file.relative_to(repo) print(f" - {rel.as_posix()}:{line_no}: {line}") if len(rows) > limit: print(f" ... ({len(rows) - limit} more)") def main() -> int: args = parse_args() repo = Path(args.repo).resolve() keyword = normalize_keyword(args.keyword) if not keyword: raise SystemExit("keyword must not be empty") java_root = repo / "src/main/java/com/vincent/rsf/server" mapper_root = repo / "src/main/resources/mapper" if not java_root.exists() or not mapper_root.exists(): raise SystemExit(f"Invalid repo root for rsf-server: {repo}") java_files = collect_files( repo, [ "src/main/java/com/vincent/rsf/server/**/*.java", "src/main/resources/mapper/**/*.xml", ], ) name_matches = find_name_matches(java_files, keyword) controller_files = collect_files(repo, ["src/main/java/com/vincent/rsf/server/**/controller/**/*.java"]) mapping_pattern = re.compile(r"@(RequestMapping|GetMapping|PostMapping|PutMapping|DeleteMapping)") authority_pattern = re.compile(r"@PreAuthorize") endpoint_rows = find_line_matches(controller_files, keyword, mapping_pattern) authority_rows = find_line_matches(controller_files, keyword, authority_pattern) print(f"repo: {repo}") print(f"keyword: {keyword}") print_file_section("Path matches", repo, name_matches, args.limit) print_line_section("Endpoint annotation matches", repo, endpoint_rows, args.limit) print_line_section("Authority annotation matches", repo, authority_rows, args.limit) return 0 if __name__ == "__main__": raise SystemExit(main())