diff --git a/scripts/analyze_code_files.py b/scripts/analyze_code_files.py index 21bd9d7e02e..1500622b140 100644 --- a/scripts/analyze_code_files.py +++ b/scripts/analyze_code_files.py @@ -40,33 +40,20 @@ SKIP_SHORT_PATTERNS = { } SKIP_SHORT_SUFFIXES = ('-cli.ts',) -# Function names to skip in duplicate detection (common utilities, test helpers) +# Function names to skip in duplicate detection. +# Only list names so generic they're expected to appear independently in many modules. +# Do NOT use prefix-based skipping — it hides real duplication (e.g. formatDuration, +# stripPrefix, parseConfig are specific enough to flag). SKIP_DUPLICATE_FUNCTIONS = { - # Common utility names + # Lifecycle / framework plumbing 'main', 'init', 'setup', 'teardown', 'cleanup', 'dispose', 'destroy', 'open', 'close', 'connect', 'disconnect', 'execute', 'run', 'start', 'stop', 'render', 'update', 'refresh', 'reset', 'clear', 'flush', + # Too-short / too-generic identifiers + 'text', 'json', 'pad', 'mask', 'digest', 'confirm', 'intro', 'outro', + 'exists', 'send', 'receive', 'listen', 'log', 'warn', 'error', 'info', + 'help', 'version', 'config', 'configure', 'describe', 'test', 'action', } - -SKIP_DUPLICATE_PREFIXES = ( - # Transformers - 'normalize', 'parse', 'validate', 'serialize', 'deserialize', - 'convert', 'transform', 'extract', 'encode', 'decode', - # Predicates - 'is', 'has', 'can', 'should', 'will', - # Constructors/factories - 'create', 'make', 'build', 'generate', 'new', - # Accessors - 'get', 'set', 'read', 'write', 'load', 'save', 'fetch', - # Handlers - 'handle', 'on', 'emit', - # Modifiers - 'add', 'remove', 'delete', 'update', 'insert', 'append', - # Other common - 'to', 'from', 'with', 'apply', 'process', 'resolve', 'ensure', 'check', - 'filter', 'map', 'reduce', 'merge', 'split', 'join', 'find', 'search', - 'register', 'unregister', 'subscribe', 'unsubscribe', -) SKIP_DUPLICATE_FILE_PATTERNS = ('.test.ts', '.test.tsx', '.spec.ts') # Known packages in the monorepo @@ -150,12 +137,33 @@ def find_duplicate_functions(files: List[Tuple[Path, int]], root_dir: Path) -> D # Skip known common function names if func in SKIP_DUPLICATE_FUNCTIONS: continue - if any(func.startswith(prefix) for prefix in SKIP_DUPLICATE_PREFIXES): - continue function_locations[func].append(file_path) - # Filter to only duplicates - return {name: paths for name, paths in function_locations.items() if len(paths) > 1} + # Filter to only duplicates, ignoring cross-extension duplicates. + # Extensions are independent packages — the same function name in + # extensions/telegram and extensions/discord is expected, not duplication. + result: Dict[str, List[Path]] = {} + for name, paths in function_locations.items(): + if len(paths) < 2: + continue + # If ALL instances are in different extensions, skip + ext_dirs = set() + non_ext = False + for p in paths: + try: + rel = p.relative_to(root_dir) + parts = rel.parts + if len(parts) >= 2 and parts[0] == 'extensions': + ext_dirs.add(parts[1]) + else: + non_ext = True + except ValueError: + non_ext = True + # Skip if every instance lives in a different extension (no core overlap) + if not non_ext and len(ext_dirs) == len(paths): + continue + result[name] = paths + return result def validate_git_ref(root_dir: Path, ref: str) -> bool: @@ -285,8 +293,6 @@ def find_duplicate_regressions( for func in functions: if func in SKIP_DUPLICATE_FUNCTIONS: continue - if any(func.startswith(prefix) for prefix in SKIP_DUPLICATE_PREFIXES): - continue base_function_locations[func].append(file_path) base_dupes = {name for name, paths in base_function_locations.items() if len(paths) > 1} @@ -432,7 +438,28 @@ def main(): print() if args.strict and violations: + # Print actionable summary so contributors know what to do + print("─" * 60) + print("❌ Code size check failed\n") + if crossed: + print(f" {len(crossed)} file(s) grew past the {args.threshold}-line limit.") + if grew: + print(f" {len(grew)} file(s) already over {args.threshold} lines got larger.") + print() + print(" How to fix:") + print(" • Split large files into smaller, focused modules") + print(" • Extract helpers, types, or constants into separate files") + print(" • See AGENTS.md for guidelines (~500-700 LOC target)") + print() + print(f" This check compares your PR against {args.compare_to}.") + print(f" Only code files are checked ({', '.join(sorted(e for e in CODE_EXTENSIONS))}).") + print(" Docs, tests names, and config files are not affected.") + print("─" * 60) sys.exit(1) + elif args.strict: + print("─" * 60) + print("✅ Code size check passed — no files exceed thresholds.") + print("─" * 60) return diff --git a/src/plugins/source-display.ts b/src/plugins/source-display.ts index 7660af22ca9..582f880c7f2 100644 --- a/src/plugins/source-display.ts +++ b/src/plugins/source-display.ts @@ -23,7 +23,8 @@ function tryRelative(root: string, filePath: string): string | null { if (path.isAbsolute(rel)) { return null; } - return rel; + // Normalize to forward slashes for display (path.relative uses backslashes on Windows) + return rel.replaceAll("\\", "/"); } export function resolvePluginSourceRoots(params: { workspaceDir?: string }): PluginSourceRoots {