From c54d27b59126b0a6b4558d8a9dfc6b4fcf6f0ac3 Mon Sep 17 00:00:00 2001 From: TheBinaryAVA Date: Wed, 6 May 2026 22:14:59 +0530 Subject: [PATCH 1/4] Smart code reviewer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Added a Smart Code Reviewer CLI tool using Python AST. ## Features - Detects long functions - Flags nested loops (O(nยฒ) risk) - Checks missing docstrings ## Usage python reviewer.py --- OSPC CONTRIBUTION/README.md | 258 ++++++++++++++ OSPC CONTRIBUTION/analyzer.cpython-312.pyc | Bin 0 -> 14295 bytes OSPC CONTRIBUTION/analyzer.py | 330 ++++++++++++++++++ OSPC CONTRIBUTION/requirements.txt | 14 + OSPC CONTRIBUTION/reviewer.py | 201 +++++++++++ .../test_analyzer.cpython-312.pyc | Bin 0 -> 17372 bytes OSPC CONTRIBUTION/test_analyzer.py | 313 +++++++++++++++++ OSPC CONTRIBUTION/utils.cpython-312.pyc | Bin 0 -> 12365 bytes OSPC CONTRIBUTION/utils.py | 252 +++++++++++++ 9 files changed, 1368 insertions(+) create mode 100644 OSPC CONTRIBUTION/README.md create mode 100644 OSPC CONTRIBUTION/analyzer.cpython-312.pyc create mode 100644 OSPC CONTRIBUTION/analyzer.py create mode 100644 OSPC CONTRIBUTION/requirements.txt create mode 100644 OSPC CONTRIBUTION/reviewer.py create mode 100644 OSPC CONTRIBUTION/test_analyzer.cpython-312.pyc create mode 100644 OSPC CONTRIBUTION/test_analyzer.py create mode 100644 OSPC CONTRIBUTION/utils.cpython-312.pyc create mode 100644 OSPC CONTRIBUTION/utils.py diff --git a/OSPC CONTRIBUTION/README.md b/OSPC CONTRIBUTION/README.md new file mode 100644 index 0000000..02186a3 --- /dev/null +++ b/OSPC CONTRIBUTION/README.md @@ -0,0 +1,258 @@ +# Smart Code Reviewer ๐Ÿ” +BY AVANTHIKA + +A zero-dependency Python static analysis tool that reviews your Python source +files and reports on **code quality**, **time-complexity patterns**, and +**best-practice violations** โ€” all from the command line. + +Built entirely with the Python standard library (`ast`, `argparse`, `json`). + +--- + +## Features + +| Category | Check | +|---|---| +| **Complexity** | Nested loops (possible O(nยฒ)) | +| **Best Practices** | Missing module / class / function docstrings | +| **Best Practices** | `while` loops without obvious termination | +| **Best Practices** | Functions with too many arguments (>5) | +| **Style** | Long functions (>50 lines) | +| **Style** | Non-snake_case function names | + +Additional capabilities: + +- โœ… Analyse **multiple files** in one command +- โœ… **JSON output** mode (`--json`) for CI/CD pipelines +- โœ… **Adjustable thresholds** (`--max-lines`, `--max-args`) +- โœ… Colour output with `NO_COLOR` support +- โœ… Meaningful exit codes for scripting + +--- + +## Requirements + +- Python **3.8+** +- No external packages + +--- + +## Installation + +```bash +# Clone / download the project +git clone https://github.com/yourname/smart-code-reviewer.git +cd smart-code-reviewer + +# (Optional) make the CLI executable +chmod +x reviewer.py +``` + +No `pip install` step required. + +--- + +## Usage + +### Basic review + +```bash +python reviewer.py myfile.py +``` + +### Review multiple files + +```bash +python reviewer.py src/main.py src/utils.py src/models.py +``` + +### Glob expansion (shell feature) + +```bash +python reviewer.py src/*.py +``` + +### JSON output (great for CI) + +```bash +python reviewer.py myfile.py --json +``` + +### Custom thresholds + +```bash +# Flag functions longer than 30 lines, or with more than 4 args +python reviewer.py myfile.py --max-lines 30 --max-args 4 +``` + +### Disable colour (e.g. when piping output) + +```bash +python reviewer.py myfile.py --no-color +# or use the standard env var: +NO_COLOR=1 python reviewer.py myfile.py +``` + +### Full help + +```bash +python reviewer.py --help +``` + +--- + +## Example output + +``` +================================================================ + Smart Code Reviewer examples/sample_bad.py +================================================================ + Lines: 72 Functions: 4 Classes: 1 + Found 8 issues: 2 warnings, 3 warnings, 3 infos + + โ”€โ”€ Complexity โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + โš  WARNING line 18 [process_data] + Nested for-loop detected. This may indicate O(nยฒ) or worse time complexity. + + โ”€โ”€ Best Practices โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + โš  WARNING line 12 [DataLoader] + Class is missing a docstring. + + โš  WARNING line 25 [process_data] + Public function is missing a docstring. + + โš  WARNING line 40 [connect] + Function has 6 arguments (threshold: 5). Consider using a config object or dataclass. + + โ„น INFO line 34 + while-loop found. Ensure the termination condition is guaranteed to prevent infinite loops. + + โ”€โ”€ Style โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + โš  WARNING line 25 [process_data] + Function is 52 lines long (threshold: 50). + + โ„น INFO line 60 [loadData] + Function name 'loadData' does not follow snake_case convention. + +================================================================ +``` + +### JSON output + +```json +[ + { + "filepath": "examples/sample_bad.py", + "summary": { + "total_lines": 72, + "total_functions": 4, + "total_classes": 1, + "total_issues": 7, + "errors": 0, + "warnings": 5, + "infos": 2 + }, + "findings": [ + { + "category": "Complexity", + "severity": "warning", + "line": 18, + "symbol": "process_data", + "message": "Nested for-loop detected. This may indicate O(nยฒ) or worse time complexity." + } + ] + } +] +``` + +--- + +## Running the tests + +```bash +# From the project root +python -m unittest discover -s tests -v +``` + +Expected output: + +``` +test_acceptable_arg_count_not_flagged (test_analyzer.TestTooManyArguments) ... ok +test_camel_case_function_flagged (test_analyzer.TestNamingConvention) ... ok +test_class_without_docstring_flagged (test_analyzer.TestMissingDocstrings) ... ok +... +---------------------------------------------------------------------- +Ran 18 tests in 0.042s + +OK +``` + +--- + +## Project structure + +``` +smart-code-reviewer/ +โ”œโ”€โ”€ reviewer.py # CLI entry point (argparse, exit codes) +โ”œโ”€โ”€ analyzer.py # Core AST visitor + data model +โ”œโ”€โ”€ utils.py # Colour output, report formatting, JSON helpers +โ”œโ”€โ”€ requirements.txt # No external deps โ€” standard library only +โ”œโ”€โ”€ README.md +โ””โ”€โ”€ tests/ + โ””โ”€โ”€ test_analyzer.py # 18 unit tests covering all checks +``` + +--- + +## Exit codes + +| Code | Meaning | +|---|---| +| `0` | No issues found | +| `1` | Warnings / info found | +| `2` | Errors found | +| `3` | File read / parse failure | +| `4` | Bad CLI arguments | + +Useful for CI pipelines: + +```bash +python reviewer.py src/main.py || echo "Review failed" +``` + +--- + +## Configuration reference + +| Flag | Default | Description | +|---|---|---| +| `--json` | off | Output results as JSON | +| `--no-color` | off | Disable ANSI colour | +| `--max-lines N` | 50 | Max lines per function | +| `--max-args N` | 5 | Max function arguments | + +--- + +## Extending the tool + +All analysis logic lives in `analyzer.py` as an `ast.NodeVisitor` subclass. +To add a new check: + +1. Add a `visit_` method to `CodeAnalyzerVisitor`. +2. Call `self._add(category, severity, line, message)` to record a finding. +3. Add a corresponding unit test in `tests/test_analyzer.py`. + +--- + +## Contributing + +Pull requests are welcome. Please: +- Follow PEP 8 +- Add tests for any new check +- Keep zero external dependencies + +--- + +## License + +MIT ยฉ 2026 diff --git a/OSPC CONTRIBUTION/analyzer.cpython-312.pyc b/OSPC CONTRIBUTION/analyzer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aa96f7ce251b85d863d9d94848e1962ee6dfe1a8 GIT binary patch literal 14295 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 str: + loc = f"line {self.line}" if self.line else "file-level" + sym = f" [{self.symbol}]" if self.symbol else "" + return f" [{self.severity.upper():7s}] {loc}{sym}: {self.message}" + + +@dataclass +class AnalysisResult: + """Aggregated results for a single file.""" + + filepath: str + findings: List[Finding] = field(default_factory=list) + total_functions: int = 0 + total_classes: int = 0 + total_lines: int = 0 + + # Convenience helpers + def by_category(self, category: str) -> List[Finding]: + return [f for f in self.findings if f.category == category] + + def error_count(self) -> int: + return sum(1 for f in self.findings if f.severity == "error") + + def warning_count(self) -> int: + return sum(1 for f in self.findings if f.severity == "warning") + + def info_count(self) -> int: + return sum(1 for f in self.findings if f.severity == "info") + + +# --------------------------------------------------------------------------- +# Configuration (thresholds) +# --------------------------------------------------------------------------- + +class ReviewConfig: + """Centralised thresholds so they're easy to tune.""" + + MAX_FUNCTION_LINES: int = 50 + MAX_FUNCTION_ARGS: int = 5 + CATEGORIES = ("Complexity", "Best Practices", "Style") + + +# --------------------------------------------------------------------------- +# Visitor +# --------------------------------------------------------------------------- + +class CodeAnalyzerVisitor(ast.NodeVisitor): + """ + Walks an AST and accumulates findings. + + Call `.visit(tree)` then read `.findings`, `.func_count`, `.class_count`. + """ + + def __init__(self, source_lines: List[str], config: ReviewConfig): + self._lines = source_lines + self._cfg = config + self.findings: List[Finding] = [] + self.func_count: int = 0 + self.class_count: int = 0 + + # Tracks nesting depth so we can flag nested loops. + self._loop_depth: int = 0 + + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + + def _add( + self, + category: str, + severity: str, + line: Optional[int], + message: str, + symbol: str = "", + ) -> None: + self.findings.append( + Finding( + category=category, + severity=severity, + line=line, + message=message, + symbol=symbol, + ) + ) + + def _function_line_count(self, node: ast.FunctionDef) -> int: + """Return the number of source lines spanned by a function node.""" + return node.end_lineno - node.lineno + 1 # type: ignore[attr-defined] + + def _has_docstring(self, node: ast.AST) -> bool: + """Return True if the first statement is a string literal (docstring).""" + body = getattr(node, "body", []) + if body and isinstance(body[0], ast.Expr): + val = body[0].value + return isinstance(val, ast.Constant) and isinstance(val.value, str) + return False + + # ------------------------------------------------------------------ + # Visitors + # ------------------------------------------------------------------ + + def visit_FunctionDef(self, node: ast.FunctionDef) -> None: # noqa: N802 + self._check_function(node) + self.generic_visit(node) + + # async defs get the same treatment + visit_AsyncFunctionDef = visit_FunctionDef # type: ignore[assignment] + + def visit_ClassDef(self, node: ast.ClassDef) -> None: # noqa: N802 + self.class_count += 1 + if not self._has_docstring(node): + self._add( + "Best Practices", + "warning", + node.lineno, + "Class is missing a docstring.", + symbol=node.name, + ) + self.generic_visit(node) + + def visit_For(self, node: ast.For) -> None: # noqa: N802 + self._check_loop(node, loop_type="for") + + def visit_While(self, node: ast.While) -> None: # noqa: N802 + self._check_loop(node, loop_type="while") + + # ------------------------------------------------------------------ + # Check helpers + # ------------------------------------------------------------------ + + def _check_function(self, node: ast.FunctionDef) -> None: + self.func_count += 1 + name = node.name + + # --- Style: long function --- + line_count = self._function_line_count(node) + if line_count > self._cfg.MAX_FUNCTION_LINES: + self._add( + "Style", + "warning", + node.lineno, + f"Function is {line_count} lines long " + f"(threshold: {self._cfg.MAX_FUNCTION_LINES}).", + symbol=name, + ) + + # --- Best Practices: missing docstring --- + if not self._has_docstring(node) and not name.startswith("_"): + self._add( + "Best Practices", + "warning", + node.lineno, + "Public function is missing a docstring.", + symbol=name, + ) + + # --- Best Practices: too many arguments --- + n_args = len(node.args.args) + if n_args > self._cfg.MAX_FUNCTION_ARGS: + self._add( + "Best Practices", + "warning", + node.lineno, + f"Function has {n_args} arguments " + f"(threshold: {self._cfg.MAX_FUNCTION_ARGS}). " + "Consider using a config object or dataclass.", + symbol=name, + ) + + # --- Style: naming convention (should be snake_case) --- + if not _is_snake_case(name) and not name.startswith("__"): + self._add( + "Style", + "info", + node.lineno, + f"Function name '{name}' does not follow snake_case convention.", + symbol=name, + ) + + def _check_loop(self, node: ast.AST, loop_type: str) -> None: + if self._loop_depth > 0: + # We're already inside a loop โ€” this is a nested loop. + self._add( + "Complexity", + "warning", + node.lineno, # type: ignore[attr-defined] + f"Nested {loop_type}-loop detected. " + "This may indicate O(nยฒ) or worse time complexity.", + ) + + if loop_type == "while": + self._add( + "Best Practices", + "info", + node.lineno, # type: ignore[attr-defined] + "while-loop found. Ensure the termination condition is " + "guaranteed to prevent infinite loops.", + ) + + # Recurse with incremented depth. + self._loop_depth += 1 + self.generic_visit(node) + self._loop_depth -= 1 + + +# --------------------------------------------------------------------------- +# Module-level checks (run on the whole tree, not per-node) +# --------------------------------------------------------------------------- + +def _check_module_docstring(tree: ast.Module) -> Optional[Finding]: + """Return a finding if the module lacks a module-level docstring.""" + body = tree.body + if body and isinstance(body[0], ast.Expr): + val = body[0].value + if isinstance(val, ast.Constant) and isinstance(val.value, str): + return None # has docstring + return Finding( + category="Best Practices", + severity="info", + line=None, + message="Module is missing a module-level docstring.", + ) + + +# --------------------------------------------------------------------------- +# Utility +# --------------------------------------------------------------------------- + +def _is_snake_case(name: str) -> bool: + """ + Return True when *name* looks like valid Python snake_case. + + Dunder methods like __init__ are excluded by the caller. + """ + return name == name.lower() and not name[0].isdigit() + + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- + +class FileAnalyzer: + """ + High-level interface: parse a file and return an :class:`AnalysisResult`. + + Example:: + + result = FileAnalyzer("mymodule.py").analyze() + for finding in result.findings: + print(finding) + """ + + def __init__(self, filepath: str, config: Optional[ReviewConfig] = None): + self.filepath = filepath + self.config = config or ReviewConfig() + + def analyze(self) -> AnalysisResult: + """ + Read, parse, and analyse the target file. + + Raises: + FileNotFoundError: if the file path does not exist. + SyntaxError: if the file contains invalid Python syntax. + OSError: for other I/O related errors. + """ + source = self._read_source() + tree = self._parse(source) + source_lines = source.splitlines() + + result = AnalysisResult( + filepath=self.filepath, + total_lines=len(source_lines), + ) + + # Module-level docstring check + mod_finding = _check_module_docstring(tree) + if mod_finding: + result.findings.append(mod_finding) + + # Walk AST + visitor = CodeAnalyzerVisitor(source_lines, self.config) + visitor.visit(tree) + + result.findings.extend(visitor.findings) + result.total_functions = visitor.func_count + result.total_classes = visitor.class_count + + return result + + # ------------------------------------------------------------------ + # Private helpers + # ------------------------------------------------------------------ + + def _read_source(self) -> str: + """Read the file and return its content as a string.""" + with open(self.filepath, "r", encoding="utf-8") as fh: + return fh.read() + + def _parse(self, source: str) -> ast.Module: + """Parse source into an AST, raising SyntaxError on failure.""" + return ast.parse(source, filename=self.filepath) diff --git a/OSPC CONTRIBUTION/requirements.txt b/OSPC CONTRIBUTION/requirements.txt new file mode 100644 index 0000000..b0d40e9 --- /dev/null +++ b/OSPC CONTRIBUTION/requirements.txt @@ -0,0 +1,14 @@ +# Smart Code Reviewer โ€” Python dependencies +# +# This tool uses ONLY the Python standard library. +# No third-party packages are required. +# +# Minimum Python version: 3.8 +# (ast.FunctionDef.end_lineno was added in 3.8) +# +# To run tests: +# python -m unittest discover -s tests +# +# Optional (for development / linting): +# pycodestyle>=2.11 +# mypy>=1.9 diff --git a/OSPC CONTRIBUTION/reviewer.py b/OSPC CONTRIBUTION/reviewer.py new file mode 100644 index 0000000..ed73a67 --- /dev/null +++ b/OSPC CONTRIBUTION/reviewer.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +""" +reviewer.py - CLI entry point for Smart Code Reviewer. + +Usage examples: + python reviewer.py myfile.py + python reviewer.py src/main.py src/utils.py + python reviewer.py *.py --json + python reviewer.py myfile.py --no-color +""" + +import argparse +import os +import sys + +# Allow running from any working directory (add package root to path). +sys.path.insert(0, os.path.dirname(__file__)) + +from analyzer import FileAnalyzer, ReviewConfig +from utils import print_json, print_multi_summary, print_report + + +# --------------------------------------------------------------------------- +# Exit codes (follow UNIX conventions) +# --------------------------------------------------------------------------- + +EXIT_OK = 0 # No issues found +EXIT_ISSUES = 1 # Issues found (warnings / infos) +EXIT_ERRORS = 2 # Hard errors found +EXIT_IO_ERROR = 3 # File read / parse failure +EXIT_USAGE_ERROR = 4 # Bad CLI arguments + + +# --------------------------------------------------------------------------- +# CLI definition +# --------------------------------------------------------------------------- + +def build_parser() -> argparse.ArgumentParser: + """Return the configured argument parser.""" + parser = argparse.ArgumentParser( + prog="reviewer", + description=( + "Smart Code Reviewer โ€” static analysis tool for Python files.\n" + "Analyses code quality, time complexity heuristics, and best-practice violations." + ), + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +examples: + python reviewer.py myfile.py + python reviewer.py src/a.py src/b.py + python reviewer.py myfile.py --json + python reviewer.py myfile.py --max-lines 30 --max-args 4 + """, + ) + + parser.add_argument( + "files", + metavar="FILE", + nargs="+", + help="One or more Python source files to analyse.", + ) + + parser.add_argument( + "--json", + action="store_true", + default=False, + help="Output results as JSON instead of the human-readable report.", + ) + + parser.add_argument( + "--no-color", + action="store_true", + default=False, + help="Disable ANSI colour output.", + ) + + parser.add_argument( + "--max-lines", + metavar="N", + type=int, + default=ReviewConfig.MAX_FUNCTION_LINES, + help=( + f"Maximum lines per function before a style warning is raised " + f"(default: {ReviewConfig.MAX_FUNCTION_LINES})." + ), + ) + + parser.add_argument( + "--max-args", + metavar="N", + type=int, + default=ReviewConfig.MAX_FUNCTION_ARGS, + help=( + f"Maximum function arguments before a best-practice warning is raised " + f"(default: {ReviewConfig.MAX_FUNCTION_ARGS})." + ), + ) + + return parser + + +# --------------------------------------------------------------------------- +# Core runner +# --------------------------------------------------------------------------- + +def run(args: argparse.Namespace) -> int: + """ + Execute the review for all specified files. + + Returns an exit code (see module-level constants). + """ + # Honour --no-color by patching the environment variable that utils.py + # respects without needing to pass state through function arguments. + if args.no_color: + os.environ["NO_COLOR"] = "1" + + # Build shared config from CLI flags. + config = ReviewConfig() + config.MAX_FUNCTION_LINES = args.max_lines + config.MAX_FUNCTION_ARGS = args.max_args + + results = [] + had_io_error = False + + for filepath in args.files: + # Validate extension (allow but warn on non-.py files). + if not filepath.endswith(".py"): + _warn(f"'{filepath}' does not appear to be a Python file. Skipping.") + continue + + try: + analyzer = FileAnalyzer(filepath, config=config) + result = analyzer.analyze() + results.append(result) + + except FileNotFoundError: + _error(f"File not found: {filepath}") + had_io_error = True + + except SyntaxError as exc: + _error( + f"Syntax error in '{filepath}' at line {exc.lineno}: {exc.msg}" + ) + had_io_error = True + + except OSError as exc: + _error(f"Cannot read '{filepath}': {exc.strerror}") + had_io_error = True + + if not results: + _error("No files were successfully analysed.") + return EXIT_IO_ERROR if had_io_error else EXIT_USAGE_ERROR + + # ---- Output ---- + if args.json: + print_json(results) + else: + for result in results: + print_report(result) + if len(results) > 1: + print_multi_summary(results) + + # ---- Determine exit code ---- + if had_io_error: + return EXIT_IO_ERROR + + total_errors = sum(r.error_count() for r in results) + if total_errors: + return EXIT_ERRORS + + total_issues = sum(len(r.findings) for r in results) + if total_issues: + return EXIT_ISSUES + + return EXIT_OK + + +# --------------------------------------------------------------------------- +# Output helpers +# --------------------------------------------------------------------------- + +def _warn(message: str) -> None: + print(f"[WARNING] {message}", file=sys.stderr) + + +def _error(message: str) -> None: + print(f"[ERROR] {message}", file=sys.stderr) + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + +def main() -> None: + parser = build_parser() + args = parser.parse_args() + sys.exit(run(args)) + + +if __name__ == "__main__": + main() diff --git a/OSPC CONTRIBUTION/test_analyzer.cpython-312.pyc b/OSPC CONTRIBUTION/test_analyzer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6eaa4c5d550ecf23123a3602e8df0eca6aaad625 GIT binary patch literal 17372 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 literal 0 HcmV?d00001 diff --git a/OSPC CONTRIBUTION/test_analyzer.py b/OSPC CONTRIBUTION/test_analyzer.py new file mode 100644 index 0000000..7450df0 --- /dev/null +++ b/OSPC CONTRIBUTION/test_analyzer.py @@ -0,0 +1,313 @@ +""" +tests/test_analyzer.py - Unit tests for Smart Code Reviewer. + +Run with: + python -m unittest discover -s tests + # or directly: + python tests/test_analyzer.py +""" + +import os +import sys +import textwrap +import unittest + +# Ensure the package root is importable regardless of where tests are run. +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from analyzer import FileAnalyzer, ReviewConfig + + +# --------------------------------------------------------------------------- +# Helper +# --------------------------------------------------------------------------- + +def _analyze_source(source: str, **config_kwargs) -> list: + """ + Parse *source* from a string and return the list of findings. + + Writes a temporary file because :class:`FileAnalyzer` works on paths. + Uses a fixed temporary file name that is cleaned up after each test. + """ + import tempfile + + config = ReviewConfig() + for key, value in config_kwargs.items(): + setattr(config, key.upper(), value) + + with tempfile.NamedTemporaryFile( + mode="w", suffix=".py", delete=False, encoding="utf-8" + ) as tmp: + tmp.write(textwrap.dedent(source)) + tmp_path = tmp.name + + try: + result = FileAnalyzer(tmp_path, config=config).analyze() + return result.findings + finally: + os.unlink(tmp_path) + + +def _categories(findings) -> list: + return [f.category for f in findings] + + +def _messages(findings) -> list: + return [f.message for f in findings] + + +def _severities(findings) -> list: + return [f.severity for f in findings] + + +# --------------------------------------------------------------------------- +# Test cases +# --------------------------------------------------------------------------- + +class TestMissingDocstrings(unittest.TestCase): + """Missing docstring checks (Best Practices).""" + + def test_function_without_docstring_flagged(self): + source = """\ + def add(a, b): + return a + b + """ + findings = _analyze_source(source) + self.assertTrue( + any("docstring" in m.lower() for m in _messages(findings)), + "Expected a missing-docstring finding.", + ) + + def test_function_with_docstring_not_flagged(self): + source = """\ + def add(a, b): + \"\"\"Return the sum of a and b.\"\"\" + return a + b + """ + findings = _analyze_source(source) + # Only check function-level docstring findings (exclude file-level) + bp_msgs = [ + f.message for f in findings + if f.category == "Best Practices" + and "docstring" in f.message.lower() + and f.line is not None # file-level findings have line=None + ] + self.assertEqual(bp_msgs, [], "Clean function should not be flagged.") + + def test_private_function_docstring_not_required(self): + source = """\ + def _helper(x): + return x * 2 + """ + findings = _analyze_source(source) + bp_msgs = [ + f.message for f in findings + if "docstring" in f.message.lower() + and f.line is not None # exclude file-level module finding + ] + self.assertEqual(bp_msgs, [], "Private functions should not require a docstring.") + + def test_class_without_docstring_flagged(self): + source = """\ + class MyClass: + pass + """ + findings = _analyze_source(source) + self.assertTrue( + any("docstring" in m.lower() for m in _messages(findings)) + ) + + +class TestLongFunctions(unittest.TestCase): + """Long function detection (Style).""" + + def test_long_function_flagged(self): + # 10 real lines + 1 def + 1 docstring = 12 lines, but we force threshold low. + body = "\n".join(f" x_{i} = {i}" for i in range(10)) + source = f'def long_func():\n """Docstring."""\n{body}\n' + findings = _analyze_source(source, max_function_lines=5) + style_msgs = [f for f in findings if f.category == "Style"] + self.assertTrue( + any("lines long" in m.message for m in style_msgs), + "Expected a 'lines long' style finding.", + ) + + def test_short_function_not_flagged(self): + source = """\ + def short(): + \"\"\"Docstring.\"\"\" + return 1 + """ + findings = _analyze_source(source, max_function_lines=50) + style_msgs = [f for f in findings if f.category == "Style" and "lines long" in f.message] + self.assertEqual(style_msgs, []) + + +class TestTooManyArguments(unittest.TestCase): + """Excessive argument count detection (Best Practices).""" + + def test_too_many_args_flagged(self): + source = """\ + def func(a, b, c, d, e, f): + \"\"\"Six args.\"\"\" + pass + """ + findings = _analyze_source(source, max_function_args=5) + self.assertTrue( + any("arguments" in m.lower() for m in _messages(findings)) + ) + + def test_acceptable_arg_count_not_flagged(self): + source = """\ + def func(a, b, c): + \"\"\"Three args โ€” fine.\"\"\" + pass + """ + findings = _analyze_source(source, max_function_args=5) + self.assertFalse( + any("arguments" in m.lower() for m in _messages(findings)) + ) + + +class TestNestedLoops(unittest.TestCase): + """Nested loop / complexity detection.""" + + def test_nested_for_loops_flagged(self): + source = """\ + def process(data): + \"\"\"Nested loops.\"\"\" + for i in data: + for j in data: + pass + """ + findings = _analyze_source(source) + complexity_msgs = [f for f in findings if f.category == "Complexity"] + self.assertTrue( + any("nested" in m.message.lower() for m in complexity_msgs), + "Expected a nested-loop finding.", + ) + + def test_single_loop_not_flagged(self): + source = """\ + def process(data): + \"\"\"Single loop.\"\"\" + for i in data: + pass + """ + findings = _analyze_source(source) + complexity_msgs = [f for f in findings if f.category == "Complexity"] + self.assertEqual(complexity_msgs, []) + + def test_nested_while_loop_flagged(self): + source = """\ + def process(n): + \"\"\"Nested while.\"\"\" + i = 0 + while i < n: + j = 0 + while j < n: + j += 1 + i += 1 + """ + findings = _analyze_source(source) + complexity_msgs = [f for f in findings if f.category == "Complexity"] + self.assertTrue(len(complexity_msgs) >= 1) + + +class TestWhileLoopWarning(unittest.TestCase): + """While loop termination warnings.""" + + def test_while_loop_info_emitted(self): + source = """\ + def run(): + \"\"\"Has a while loop.\"\"\" + i = 0 + while i < 10: + i += 1 + """ + findings = _analyze_source(source) + bp_msgs = [f for f in findings if f.category == "Best Practices"] + self.assertTrue( + any("while" in m.message.lower() for m in bp_msgs) + ) + + +class TestNamingConvention(unittest.TestCase): + """snake_case convention checks (Style).""" + + def test_camel_case_function_flagged(self): + source = """\ + def myFunction(): + \"\"\"Not snake_case.\"\"\" + pass + """ + findings = _analyze_source(source) + style_msgs = [f for f in findings if f.category == "Style"] + self.assertTrue( + any("snake_case" in m.message for m in style_msgs) + ) + + def test_snake_case_function_not_flagged(self): + source = """\ + def my_function(): + \"\"\"Proper snake_case.\"\"\" + pass + """ + findings = _analyze_source(source) + style_msgs = [ + f for f in findings + if f.category == "Style" and "snake_case" in f.message + ] + self.assertEqual(style_msgs, []) + + +class TestErrorHandling(unittest.TestCase): + """Error handling โ€” FileAnalyzer should raise standard exceptions.""" + + def test_file_not_found(self): + with self.assertRaises(FileNotFoundError): + FileAnalyzer("/nonexistent/path/to/file.py").analyze() + + def test_syntax_error(self): + import tempfile + + with tempfile.NamedTemporaryFile( + mode="w", suffix=".py", delete=False, encoding="utf-8" + ) as tmp: + tmp.write("def broken(\n") + tmp_path = tmp.name + + try: + with self.assertRaises(SyntaxError): + FileAnalyzer(tmp_path).analyze() + finally: + os.unlink(tmp_path) + + +class TestModuleDocstring(unittest.TestCase): + """Module-level docstring check.""" + + def test_module_without_docstring_flagged(self): + source = """\ + x = 1 + """ + findings = _analyze_source(source) + file_level = [f for f in findings if f.line is None] + self.assertTrue(len(file_level) >= 1) + + def test_module_with_docstring_not_flagged(self): + source = """\ + \"\"\"My module docstring.\"\"\" + x = 1 + """ + findings = _analyze_source(source) + file_level = [f for f in findings if f.line is None] + self.assertEqual(file_level, []) + + +# --------------------------------------------------------------------------- +# Runner +# --------------------------------------------------------------------------- + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/OSPC CONTRIBUTION/utils.cpython-312.pyc b/OSPC CONTRIBUTION/utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..86a868c114840d344ac639e13fb4da080509d787 GIT binary patch literal 12365 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 literal 0 HcmV?d00001 diff --git a/OSPC CONTRIBUTION/utils.py b/OSPC CONTRIBUTION/utils.py new file mode 100644 index 0000000..bb671e8 --- /dev/null +++ b/OSPC CONTRIBUTION/utils.py @@ -0,0 +1,252 @@ +""" +utils.py - Helper utilities for Smart Code Reviewer. + +Contains: + - ANSI colour helpers for terminal output + - JSON serialisation helpers + - Summary statistics builder +""" + +import json +import os +import sys +from typing import Dict, List + +from analyzer import AnalysisResult, Finding + + +# --------------------------------------------------------------------------- +# Terminal colour support +# --------------------------------------------------------------------------- + +# Respect NO_COLOR env var (https://no-color.org/) and non-TTY streams. +_COLOUR_ENABLED: bool = ( + sys.stdout.isatty() + and os.environ.get("NO_COLOR") is None + and os.environ.get("TERM") != "dumb" +) + + +class Colour: + """ANSI escape codes used throughout the reporter.""" + + RESET = "\033[0m" + BOLD = "\033[1m" + DIM = "\033[2m" + + RED = "\033[91m" + YELLOW = "\033[93m" + CYAN = "\033[96m" + GREEN = "\033[92m" + MAGENTA = "\033[95m" + WHITE = "\033[97m" + GREY = "\033[90m" + + @staticmethod + def apply(code: str, text: str) -> str: + """Wrap *text* in *code* if colour output is enabled.""" + if not _COLOUR_ENABLED: + return text + return f"{code}{text}{Colour.RESET}" + + # Convenience shortcuts + @staticmethod + def bold(text: str) -> str: + return Colour.apply(Colour.BOLD, text) + + @staticmethod + def red(text: str) -> str: + return Colour.apply(Colour.RED, text) + + @staticmethod + def yellow(text: str) -> str: + return Colour.apply(Colour.YELLOW, text) + + @staticmethod + def cyan(text: str) -> str: + return Colour.apply(Colour.CYAN, text) + + @staticmethod + def green(text: str) -> str: + return Colour.apply(Colour.GREEN, text) + + @staticmethod + def grey(text: str) -> str: + return Colour.apply(Colour.GREY, text) + + @staticmethod + def magenta(text: str) -> str: + return Colour.apply(Colour.MAGENTA, text) + + +# --------------------------------------------------------------------------- +# Severity helpers +# --------------------------------------------------------------------------- + +_SEVERITY_COLOUR: Dict[str, str] = { + "error": Colour.RED, + "warning": Colour.YELLOW, + "info": Colour.CYAN, +} + +_SEVERITY_ICON: Dict[str, str] = { + "error": "โœ–", + "warning": "โš ", + "info": "โ„น", +} + + +def format_severity(severity: str) -> str: + """Return a coloured, icon-prefixed severity label.""" + icon = _SEVERITY_ICON.get(severity, "ยท") + colour = _SEVERITY_COLOUR.get(severity, "") + label = f"{icon} {severity.upper()}" + return Colour.apply(colour, label) + + +# --------------------------------------------------------------------------- +# Report formatting +# --------------------------------------------------------------------------- + +CATEGORIES = ("Complexity", "Best Practices", "Style") + +_CATEGORY_COLOUR: Dict[str, str] = { + "Complexity": Colour.MAGENTA, + "Best Practices": Colour.CYAN, + "Style": Colour.YELLOW, +} + + +def format_finding(finding: Finding) -> str: + """Format a single :class:`Finding` as a human-readable string.""" + sev_label = format_severity(finding.severity) + loc = ( + Colour.grey(f"line {finding.line}") + if finding.line + else Colour.grey("file-level") + ) + sym = ( + f" {Colour.bold(finding.symbol)}" if finding.symbol else "" + ) + return f" {sev_label:<22} {loc}{sym}\n {finding.message}" + + +def print_report(result: AnalysisResult) -> None: + """Print a full human-readable report for one :class:`AnalysisResult`.""" + # ---- Header ---- + print() + print(Colour.bold("=" * 64)) + print( + Colour.bold(" Smart Code Reviewer") + + " " + + Colour.grey(result.filepath) + ) + print(Colour.bold("=" * 64)) + + # ---- Stats line ---- + stats = ( + f" {Colour.grey('Lines:')} {result.total_lines}" + f" {Colour.grey('Functions:')} {result.total_functions}" + f" {Colour.grey('Classes:')} {result.total_classes}" + ) + print(stats) + + total = len(result.findings) + if total == 0: + print() + print(Colour.green(" โœ” No issues found. Great work!")) + print() + return + + # Summary counts + errors = result.error_count() + warnings = result.warning_count() + infos = result.info_count() + summary_parts = [] + if errors: + summary_parts.append(Colour.red(f"{errors} error{'s' if errors > 1 else ''}")) + if warnings: + summary_parts.append(Colour.yellow(f"{warnings} warning{'s' if warnings > 1 else ''}")) + if infos: + summary_parts.append(Colour.cyan(f"{infos} info")) + + print(f" Found {total} issue{'s' if total > 1 else ''}: {', '.join(summary_parts)}") + + # ---- Category sections ---- + for category in CATEGORIES: + findings = result.by_category(category) + if not findings: + continue + + cat_colour = _CATEGORY_COLOUR.get(category, "") + print() + print(Colour.apply(cat_colour, Colour.bold(f" โ”€โ”€ {category} "))) + print(Colour.apply(cat_colour, " " + "โ”€" * 58)) + for finding in findings: + print(format_finding(finding)) + print() + + # ---- Footer ---- + print(Colour.bold("=" * 64)) + print() + + +def print_multi_summary(results: List[AnalysisResult]) -> None: + """Print a brief aggregate summary when multiple files are reviewed.""" + if len(results) <= 1: + return + + total_issues = sum(len(r.findings) for r in results) + total_errors = sum(r.error_count() for r in results) + total_warns = sum(r.warning_count() for r in results) + + print() + print(Colour.bold("โ•" * 64)) + print(Colour.bold(" Aggregate Summary")) + print(Colour.bold("โ•" * 64)) + print(f" Files analysed : {len(results)}") + print(f" Total issues : {total_issues}") + print( + f" Errors : {Colour.red(str(total_errors))}" + f" Warnings : {Colour.yellow(str(total_warns))}" + ) + clean = sum(1 for r in results if len(r.findings) == 0) + print(f" Clean files : {Colour.green(str(clean))}") + print(Colour.bold("โ•" * 64)) + print() + + +# --------------------------------------------------------------------------- +# JSON serialisation +# --------------------------------------------------------------------------- + +def result_to_dict(result: AnalysisResult) -> dict: + """Convert an :class:`AnalysisResult` to a plain dict (JSON-serialisable).""" + return { + "filepath": result.filepath, + "summary": { + "total_lines": result.total_lines, + "total_functions": result.total_functions, + "total_classes": result.total_classes, + "total_issues": len(result.findings), + "errors": result.error_count(), + "warnings": result.warning_count(), + "infos": result.info_count(), + }, + "findings": [ + { + "category": f.category, + "severity": f.severity, + "line": f.line, + "symbol": f.symbol, + "message": f.message, + } + for f in result.findings + ], + } + + +def print_json(results: List[AnalysisResult]) -> None: + """Serialise results to JSON and write to stdout.""" + payload = [result_to_dict(r) for r in results] + print(json.dumps(payload, indent=2)) From a3c93b9484930b59aa867911fb0513115b8b4ba6 Mon Sep 17 00:00:00 2001 From: TheBinaryAVA Date: Wed, 6 May 2026 22:18:18 +0530 Subject: [PATCH 2/4] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 0ec5235..94f5e6e 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ So far, the following projects have been integrated to this repo: |[AI chatbot](Artificial-intelligence_bot) |[umar abdullahi](https://github.com/umarbrowser) | |[AI for guess the number](https://github.com/hastagAB/Awesome-Python-Scripts/tree/master/AI_for_Guess_the_number) | [Omar Sameh](https://github.com/ShadowHunter15) | |[Address locator](Location_Of_Adress) | [Chris]() | +|[AWE-SMART CODE REVIEWER](Location_Of_Adress) | [Chris]() | |[Asymmetric Encryption](asymmetric_cryptography) |[victor matheus](https://github.com/victormatheusc) | |[Attachment Unique Mail](Attachment_Unique_Mail) |[Arnav Dandekar](https://github.com/4rnv) | |[Automated calendar](automated_calendar) | [J.A. Hernรกndez](https://github.com/jesusalberto18) | @@ -121,6 +122,7 @@ So far, the following projects have been integrated to this repo: |[Remove-Duplicate-Files](Remove-Duplicate-Files)|[Aayushi Varma](https://github.com/aayuv17)| |[Rock-Paper-Scissor Game](https://github.com/hastagAB/Awesome-Python-Scripts/tree/master/Rock-Paper-Scissor)|[Punit Sakre](https://github.com/punitsakre23)| |[send_whatsapp_message](send_whatsapp_message)|[Mukesh Prasad](https://github.com/mukeshprasad)| +|[smart_code_reviewer](https://github.com/TheBinaryAVA/Awesome-Python-Scripts/tree/master/smart_code_reviewer)|[TheBinaryAVA](https://github.com/TheBinaryAVA)| |[Send messages to sqs in parallel](send_sqs_messages_in_parallel)|[Jinam Shah](https://github.com/jinamshah)| |[Server Ping](Ping_Server)|[prince]()| |[Signature photo to PNG converter](signature2png)|[Rodolfo Ferro](https://github.com/RodolfoFerro)| From c061a2c8c52d69bd473c746cbcfa87cec7817d62 Mon Sep 17 00:00:00 2001 From: TheBinaryAVA Date: Wed, 6 May 2026 22:19:23 +0530 Subject: [PATCH 3/4] Smart code reviewer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Added a Smart Code Reviewer CLI tool using Python AST. ## Features - Detects long functions - Flags nested loops (O(nยฒ) risk) - Checks missing docstrings ## Usage python reviewer.py --- smart_code_reviewer/README.md | 258 ++++++++++++++ smart_code_reviewer/analyzer.cpython-312.pyc | Bin 0 -> 14295 bytes smart_code_reviewer/analyzer.py | 330 ++++++++++++++++++ smart_code_reviewer/requirements.txt | 14 + smart_code_reviewer/reviewer.py | 201 +++++++++++ .../test_analyzer.cpython-312.pyc | Bin 0 -> 17372 bytes smart_code_reviewer/test_analyzer.py | 313 +++++++++++++++++ smart_code_reviewer/utils.cpython-312.pyc | Bin 0 -> 12365 bytes smart_code_reviewer/utils.py | 252 +++++++++++++ 9 files changed, 1368 insertions(+) create mode 100644 smart_code_reviewer/README.md create mode 100644 smart_code_reviewer/analyzer.cpython-312.pyc create mode 100644 smart_code_reviewer/analyzer.py create mode 100644 smart_code_reviewer/requirements.txt create mode 100644 smart_code_reviewer/reviewer.py create mode 100644 smart_code_reviewer/test_analyzer.cpython-312.pyc create mode 100644 smart_code_reviewer/test_analyzer.py create mode 100644 smart_code_reviewer/utils.cpython-312.pyc create mode 100644 smart_code_reviewer/utils.py diff --git a/smart_code_reviewer/README.md b/smart_code_reviewer/README.md new file mode 100644 index 0000000..02186a3 --- /dev/null +++ b/smart_code_reviewer/README.md @@ -0,0 +1,258 @@ +# Smart Code Reviewer ๐Ÿ” +BY AVANTHIKA + +A zero-dependency Python static analysis tool that reviews your Python source +files and reports on **code quality**, **time-complexity patterns**, and +**best-practice violations** โ€” all from the command line. + +Built entirely with the Python standard library (`ast`, `argparse`, `json`). + +--- + +## Features + +| Category | Check | +|---|---| +| **Complexity** | Nested loops (possible O(nยฒ)) | +| **Best Practices** | Missing module / class / function docstrings | +| **Best Practices** | `while` loops without obvious termination | +| **Best Practices** | Functions with too many arguments (>5) | +| **Style** | Long functions (>50 lines) | +| **Style** | Non-snake_case function names | + +Additional capabilities: + +- โœ… Analyse **multiple files** in one command +- โœ… **JSON output** mode (`--json`) for CI/CD pipelines +- โœ… **Adjustable thresholds** (`--max-lines`, `--max-args`) +- โœ… Colour output with `NO_COLOR` support +- โœ… Meaningful exit codes for scripting + +--- + +## Requirements + +- Python **3.8+** +- No external packages + +--- + +## Installation + +```bash +# Clone / download the project +git clone https://github.com/yourname/smart-code-reviewer.git +cd smart-code-reviewer + +# (Optional) make the CLI executable +chmod +x reviewer.py +``` + +No `pip install` step required. + +--- + +## Usage + +### Basic review + +```bash +python reviewer.py myfile.py +``` + +### Review multiple files + +```bash +python reviewer.py src/main.py src/utils.py src/models.py +``` + +### Glob expansion (shell feature) + +```bash +python reviewer.py src/*.py +``` + +### JSON output (great for CI) + +```bash +python reviewer.py myfile.py --json +``` + +### Custom thresholds + +```bash +# Flag functions longer than 30 lines, or with more than 4 args +python reviewer.py myfile.py --max-lines 30 --max-args 4 +``` + +### Disable colour (e.g. when piping output) + +```bash +python reviewer.py myfile.py --no-color +# or use the standard env var: +NO_COLOR=1 python reviewer.py myfile.py +``` + +### Full help + +```bash +python reviewer.py --help +``` + +--- + +## Example output + +``` +================================================================ + Smart Code Reviewer examples/sample_bad.py +================================================================ + Lines: 72 Functions: 4 Classes: 1 + Found 8 issues: 2 warnings, 3 warnings, 3 infos + + โ”€โ”€ Complexity โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + โš  WARNING line 18 [process_data] + Nested for-loop detected. This may indicate O(nยฒ) or worse time complexity. + + โ”€โ”€ Best Practices โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + โš  WARNING line 12 [DataLoader] + Class is missing a docstring. + + โš  WARNING line 25 [process_data] + Public function is missing a docstring. + + โš  WARNING line 40 [connect] + Function has 6 arguments (threshold: 5). Consider using a config object or dataclass. + + โ„น INFO line 34 + while-loop found. Ensure the termination condition is guaranteed to prevent infinite loops. + + โ”€โ”€ Style โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + โš  WARNING line 25 [process_data] + Function is 52 lines long (threshold: 50). + + โ„น INFO line 60 [loadData] + Function name 'loadData' does not follow snake_case convention. + +================================================================ +``` + +### JSON output + +```json +[ + { + "filepath": "examples/sample_bad.py", + "summary": { + "total_lines": 72, + "total_functions": 4, + "total_classes": 1, + "total_issues": 7, + "errors": 0, + "warnings": 5, + "infos": 2 + }, + "findings": [ + { + "category": "Complexity", + "severity": "warning", + "line": 18, + "symbol": "process_data", + "message": "Nested for-loop detected. This may indicate O(nยฒ) or worse time complexity." + } + ] + } +] +``` + +--- + +## Running the tests + +```bash +# From the project root +python -m unittest discover -s tests -v +``` + +Expected output: + +``` +test_acceptable_arg_count_not_flagged (test_analyzer.TestTooManyArguments) ... ok +test_camel_case_function_flagged (test_analyzer.TestNamingConvention) ... ok +test_class_without_docstring_flagged (test_analyzer.TestMissingDocstrings) ... ok +... +---------------------------------------------------------------------- +Ran 18 tests in 0.042s + +OK +``` + +--- + +## Project structure + +``` +smart-code-reviewer/ +โ”œโ”€โ”€ reviewer.py # CLI entry point (argparse, exit codes) +โ”œโ”€โ”€ analyzer.py # Core AST visitor + data model +โ”œโ”€โ”€ utils.py # Colour output, report formatting, JSON helpers +โ”œโ”€โ”€ requirements.txt # No external deps โ€” standard library only +โ”œโ”€โ”€ README.md +โ””โ”€โ”€ tests/ + โ””โ”€โ”€ test_analyzer.py # 18 unit tests covering all checks +``` + +--- + +## Exit codes + +| Code | Meaning | +|---|---| +| `0` | No issues found | +| `1` | Warnings / info found | +| `2` | Errors found | +| `3` | File read / parse failure | +| `4` | Bad CLI arguments | + +Useful for CI pipelines: + +```bash +python reviewer.py src/main.py || echo "Review failed" +``` + +--- + +## Configuration reference + +| Flag | Default | Description | +|---|---|---| +| `--json` | off | Output results as JSON | +| `--no-color` | off | Disable ANSI colour | +| `--max-lines N` | 50 | Max lines per function | +| `--max-args N` | 5 | Max function arguments | + +--- + +## Extending the tool + +All analysis logic lives in `analyzer.py` as an `ast.NodeVisitor` subclass. +To add a new check: + +1. Add a `visit_` method to `CodeAnalyzerVisitor`. +2. Call `self._add(category, severity, line, message)` to record a finding. +3. Add a corresponding unit test in `tests/test_analyzer.py`. + +--- + +## Contributing + +Pull requests are welcome. Please: +- Follow PEP 8 +- Add tests for any new check +- Keep zero external dependencies + +--- + +## License + +MIT ยฉ 2026 diff --git a/smart_code_reviewer/analyzer.cpython-312.pyc b/smart_code_reviewer/analyzer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aa96f7ce251b85d863d9d94848e1962ee6dfe1a8 GIT binary patch literal 14295 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 str: + loc = f"line {self.line}" if self.line else "file-level" + sym = f" [{self.symbol}]" if self.symbol else "" + return f" [{self.severity.upper():7s}] {loc}{sym}: {self.message}" + + +@dataclass +class AnalysisResult: + """Aggregated results for a single file.""" + + filepath: str + findings: List[Finding] = field(default_factory=list) + total_functions: int = 0 + total_classes: int = 0 + total_lines: int = 0 + + # Convenience helpers + def by_category(self, category: str) -> List[Finding]: + return [f for f in self.findings if f.category == category] + + def error_count(self) -> int: + return sum(1 for f in self.findings if f.severity == "error") + + def warning_count(self) -> int: + return sum(1 for f in self.findings if f.severity == "warning") + + def info_count(self) -> int: + return sum(1 for f in self.findings if f.severity == "info") + + +# --------------------------------------------------------------------------- +# Configuration (thresholds) +# --------------------------------------------------------------------------- + +class ReviewConfig: + """Centralised thresholds so they're easy to tune.""" + + MAX_FUNCTION_LINES: int = 50 + MAX_FUNCTION_ARGS: int = 5 + CATEGORIES = ("Complexity", "Best Practices", "Style") + + +# --------------------------------------------------------------------------- +# Visitor +# --------------------------------------------------------------------------- + +class CodeAnalyzerVisitor(ast.NodeVisitor): + """ + Walks an AST and accumulates findings. + + Call `.visit(tree)` then read `.findings`, `.func_count`, `.class_count`. + """ + + def __init__(self, source_lines: List[str], config: ReviewConfig): + self._lines = source_lines + self._cfg = config + self.findings: List[Finding] = [] + self.func_count: int = 0 + self.class_count: int = 0 + + # Tracks nesting depth so we can flag nested loops. + self._loop_depth: int = 0 + + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + + def _add( + self, + category: str, + severity: str, + line: Optional[int], + message: str, + symbol: str = "", + ) -> None: + self.findings.append( + Finding( + category=category, + severity=severity, + line=line, + message=message, + symbol=symbol, + ) + ) + + def _function_line_count(self, node: ast.FunctionDef) -> int: + """Return the number of source lines spanned by a function node.""" + return node.end_lineno - node.lineno + 1 # type: ignore[attr-defined] + + def _has_docstring(self, node: ast.AST) -> bool: + """Return True if the first statement is a string literal (docstring).""" + body = getattr(node, "body", []) + if body and isinstance(body[0], ast.Expr): + val = body[0].value + return isinstance(val, ast.Constant) and isinstance(val.value, str) + return False + + # ------------------------------------------------------------------ + # Visitors + # ------------------------------------------------------------------ + + def visit_FunctionDef(self, node: ast.FunctionDef) -> None: # noqa: N802 + self._check_function(node) + self.generic_visit(node) + + # async defs get the same treatment + visit_AsyncFunctionDef = visit_FunctionDef # type: ignore[assignment] + + def visit_ClassDef(self, node: ast.ClassDef) -> None: # noqa: N802 + self.class_count += 1 + if not self._has_docstring(node): + self._add( + "Best Practices", + "warning", + node.lineno, + "Class is missing a docstring.", + symbol=node.name, + ) + self.generic_visit(node) + + def visit_For(self, node: ast.For) -> None: # noqa: N802 + self._check_loop(node, loop_type="for") + + def visit_While(self, node: ast.While) -> None: # noqa: N802 + self._check_loop(node, loop_type="while") + + # ------------------------------------------------------------------ + # Check helpers + # ------------------------------------------------------------------ + + def _check_function(self, node: ast.FunctionDef) -> None: + self.func_count += 1 + name = node.name + + # --- Style: long function --- + line_count = self._function_line_count(node) + if line_count > self._cfg.MAX_FUNCTION_LINES: + self._add( + "Style", + "warning", + node.lineno, + f"Function is {line_count} lines long " + f"(threshold: {self._cfg.MAX_FUNCTION_LINES}).", + symbol=name, + ) + + # --- Best Practices: missing docstring --- + if not self._has_docstring(node) and not name.startswith("_"): + self._add( + "Best Practices", + "warning", + node.lineno, + "Public function is missing a docstring.", + symbol=name, + ) + + # --- Best Practices: too many arguments --- + n_args = len(node.args.args) + if n_args > self._cfg.MAX_FUNCTION_ARGS: + self._add( + "Best Practices", + "warning", + node.lineno, + f"Function has {n_args} arguments " + f"(threshold: {self._cfg.MAX_FUNCTION_ARGS}). " + "Consider using a config object or dataclass.", + symbol=name, + ) + + # --- Style: naming convention (should be snake_case) --- + if not _is_snake_case(name) and not name.startswith("__"): + self._add( + "Style", + "info", + node.lineno, + f"Function name '{name}' does not follow snake_case convention.", + symbol=name, + ) + + def _check_loop(self, node: ast.AST, loop_type: str) -> None: + if self._loop_depth > 0: + # We're already inside a loop โ€” this is a nested loop. + self._add( + "Complexity", + "warning", + node.lineno, # type: ignore[attr-defined] + f"Nested {loop_type}-loop detected. " + "This may indicate O(nยฒ) or worse time complexity.", + ) + + if loop_type == "while": + self._add( + "Best Practices", + "info", + node.lineno, # type: ignore[attr-defined] + "while-loop found. Ensure the termination condition is " + "guaranteed to prevent infinite loops.", + ) + + # Recurse with incremented depth. + self._loop_depth += 1 + self.generic_visit(node) + self._loop_depth -= 1 + + +# --------------------------------------------------------------------------- +# Module-level checks (run on the whole tree, not per-node) +# --------------------------------------------------------------------------- + +def _check_module_docstring(tree: ast.Module) -> Optional[Finding]: + """Return a finding if the module lacks a module-level docstring.""" + body = tree.body + if body and isinstance(body[0], ast.Expr): + val = body[0].value + if isinstance(val, ast.Constant) and isinstance(val.value, str): + return None # has docstring + return Finding( + category="Best Practices", + severity="info", + line=None, + message="Module is missing a module-level docstring.", + ) + + +# --------------------------------------------------------------------------- +# Utility +# --------------------------------------------------------------------------- + +def _is_snake_case(name: str) -> bool: + """ + Return True when *name* looks like valid Python snake_case. + + Dunder methods like __init__ are excluded by the caller. + """ + return name == name.lower() and not name[0].isdigit() + + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- + +class FileAnalyzer: + """ + High-level interface: parse a file and return an :class:`AnalysisResult`. + + Example:: + + result = FileAnalyzer("mymodule.py").analyze() + for finding in result.findings: + print(finding) + """ + + def __init__(self, filepath: str, config: Optional[ReviewConfig] = None): + self.filepath = filepath + self.config = config or ReviewConfig() + + def analyze(self) -> AnalysisResult: + """ + Read, parse, and analyse the target file. + + Raises: + FileNotFoundError: if the file path does not exist. + SyntaxError: if the file contains invalid Python syntax. + OSError: for other I/O related errors. + """ + source = self._read_source() + tree = self._parse(source) + source_lines = source.splitlines() + + result = AnalysisResult( + filepath=self.filepath, + total_lines=len(source_lines), + ) + + # Module-level docstring check + mod_finding = _check_module_docstring(tree) + if mod_finding: + result.findings.append(mod_finding) + + # Walk AST + visitor = CodeAnalyzerVisitor(source_lines, self.config) + visitor.visit(tree) + + result.findings.extend(visitor.findings) + result.total_functions = visitor.func_count + result.total_classes = visitor.class_count + + return result + + # ------------------------------------------------------------------ + # Private helpers + # ------------------------------------------------------------------ + + def _read_source(self) -> str: + """Read the file and return its content as a string.""" + with open(self.filepath, "r", encoding="utf-8") as fh: + return fh.read() + + def _parse(self, source: str) -> ast.Module: + """Parse source into an AST, raising SyntaxError on failure.""" + return ast.parse(source, filename=self.filepath) diff --git a/smart_code_reviewer/requirements.txt b/smart_code_reviewer/requirements.txt new file mode 100644 index 0000000..b0d40e9 --- /dev/null +++ b/smart_code_reviewer/requirements.txt @@ -0,0 +1,14 @@ +# Smart Code Reviewer โ€” Python dependencies +# +# This tool uses ONLY the Python standard library. +# No third-party packages are required. +# +# Minimum Python version: 3.8 +# (ast.FunctionDef.end_lineno was added in 3.8) +# +# To run tests: +# python -m unittest discover -s tests +# +# Optional (for development / linting): +# pycodestyle>=2.11 +# mypy>=1.9 diff --git a/smart_code_reviewer/reviewer.py b/smart_code_reviewer/reviewer.py new file mode 100644 index 0000000..ed73a67 --- /dev/null +++ b/smart_code_reviewer/reviewer.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +""" +reviewer.py - CLI entry point for Smart Code Reviewer. + +Usage examples: + python reviewer.py myfile.py + python reviewer.py src/main.py src/utils.py + python reviewer.py *.py --json + python reviewer.py myfile.py --no-color +""" + +import argparse +import os +import sys + +# Allow running from any working directory (add package root to path). +sys.path.insert(0, os.path.dirname(__file__)) + +from analyzer import FileAnalyzer, ReviewConfig +from utils import print_json, print_multi_summary, print_report + + +# --------------------------------------------------------------------------- +# Exit codes (follow UNIX conventions) +# --------------------------------------------------------------------------- + +EXIT_OK = 0 # No issues found +EXIT_ISSUES = 1 # Issues found (warnings / infos) +EXIT_ERRORS = 2 # Hard errors found +EXIT_IO_ERROR = 3 # File read / parse failure +EXIT_USAGE_ERROR = 4 # Bad CLI arguments + + +# --------------------------------------------------------------------------- +# CLI definition +# --------------------------------------------------------------------------- + +def build_parser() -> argparse.ArgumentParser: + """Return the configured argument parser.""" + parser = argparse.ArgumentParser( + prog="reviewer", + description=( + "Smart Code Reviewer โ€” static analysis tool for Python files.\n" + "Analyses code quality, time complexity heuristics, and best-practice violations." + ), + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +examples: + python reviewer.py myfile.py + python reviewer.py src/a.py src/b.py + python reviewer.py myfile.py --json + python reviewer.py myfile.py --max-lines 30 --max-args 4 + """, + ) + + parser.add_argument( + "files", + metavar="FILE", + nargs="+", + help="One or more Python source files to analyse.", + ) + + parser.add_argument( + "--json", + action="store_true", + default=False, + help="Output results as JSON instead of the human-readable report.", + ) + + parser.add_argument( + "--no-color", + action="store_true", + default=False, + help="Disable ANSI colour output.", + ) + + parser.add_argument( + "--max-lines", + metavar="N", + type=int, + default=ReviewConfig.MAX_FUNCTION_LINES, + help=( + f"Maximum lines per function before a style warning is raised " + f"(default: {ReviewConfig.MAX_FUNCTION_LINES})." + ), + ) + + parser.add_argument( + "--max-args", + metavar="N", + type=int, + default=ReviewConfig.MAX_FUNCTION_ARGS, + help=( + f"Maximum function arguments before a best-practice warning is raised " + f"(default: {ReviewConfig.MAX_FUNCTION_ARGS})." + ), + ) + + return parser + + +# --------------------------------------------------------------------------- +# Core runner +# --------------------------------------------------------------------------- + +def run(args: argparse.Namespace) -> int: + """ + Execute the review for all specified files. + + Returns an exit code (see module-level constants). + """ + # Honour --no-color by patching the environment variable that utils.py + # respects without needing to pass state through function arguments. + if args.no_color: + os.environ["NO_COLOR"] = "1" + + # Build shared config from CLI flags. + config = ReviewConfig() + config.MAX_FUNCTION_LINES = args.max_lines + config.MAX_FUNCTION_ARGS = args.max_args + + results = [] + had_io_error = False + + for filepath in args.files: + # Validate extension (allow but warn on non-.py files). + if not filepath.endswith(".py"): + _warn(f"'{filepath}' does not appear to be a Python file. Skipping.") + continue + + try: + analyzer = FileAnalyzer(filepath, config=config) + result = analyzer.analyze() + results.append(result) + + except FileNotFoundError: + _error(f"File not found: {filepath}") + had_io_error = True + + except SyntaxError as exc: + _error( + f"Syntax error in '{filepath}' at line {exc.lineno}: {exc.msg}" + ) + had_io_error = True + + except OSError as exc: + _error(f"Cannot read '{filepath}': {exc.strerror}") + had_io_error = True + + if not results: + _error("No files were successfully analysed.") + return EXIT_IO_ERROR if had_io_error else EXIT_USAGE_ERROR + + # ---- Output ---- + if args.json: + print_json(results) + else: + for result in results: + print_report(result) + if len(results) > 1: + print_multi_summary(results) + + # ---- Determine exit code ---- + if had_io_error: + return EXIT_IO_ERROR + + total_errors = sum(r.error_count() for r in results) + if total_errors: + return EXIT_ERRORS + + total_issues = sum(len(r.findings) for r in results) + if total_issues: + return EXIT_ISSUES + + return EXIT_OK + + +# --------------------------------------------------------------------------- +# Output helpers +# --------------------------------------------------------------------------- + +def _warn(message: str) -> None: + print(f"[WARNING] {message}", file=sys.stderr) + + +def _error(message: str) -> None: + print(f"[ERROR] {message}", file=sys.stderr) + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + +def main() -> None: + parser = build_parser() + args = parser.parse_args() + sys.exit(run(args)) + + +if __name__ == "__main__": + main() diff --git a/smart_code_reviewer/test_analyzer.cpython-312.pyc b/smart_code_reviewer/test_analyzer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6eaa4c5d550ecf23123a3602e8df0eca6aaad625 GIT binary patch literal 17372 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 literal 0 HcmV?d00001 diff --git a/smart_code_reviewer/test_analyzer.py b/smart_code_reviewer/test_analyzer.py new file mode 100644 index 0000000..7450df0 --- /dev/null +++ b/smart_code_reviewer/test_analyzer.py @@ -0,0 +1,313 @@ +""" +tests/test_analyzer.py - Unit tests for Smart Code Reviewer. + +Run with: + python -m unittest discover -s tests + # or directly: + python tests/test_analyzer.py +""" + +import os +import sys +import textwrap +import unittest + +# Ensure the package root is importable regardless of where tests are run. +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from analyzer import FileAnalyzer, ReviewConfig + + +# --------------------------------------------------------------------------- +# Helper +# --------------------------------------------------------------------------- + +def _analyze_source(source: str, **config_kwargs) -> list: + """ + Parse *source* from a string and return the list of findings. + + Writes a temporary file because :class:`FileAnalyzer` works on paths. + Uses a fixed temporary file name that is cleaned up after each test. + """ + import tempfile + + config = ReviewConfig() + for key, value in config_kwargs.items(): + setattr(config, key.upper(), value) + + with tempfile.NamedTemporaryFile( + mode="w", suffix=".py", delete=False, encoding="utf-8" + ) as tmp: + tmp.write(textwrap.dedent(source)) + tmp_path = tmp.name + + try: + result = FileAnalyzer(tmp_path, config=config).analyze() + return result.findings + finally: + os.unlink(tmp_path) + + +def _categories(findings) -> list: + return [f.category for f in findings] + + +def _messages(findings) -> list: + return [f.message for f in findings] + + +def _severities(findings) -> list: + return [f.severity for f in findings] + + +# --------------------------------------------------------------------------- +# Test cases +# --------------------------------------------------------------------------- + +class TestMissingDocstrings(unittest.TestCase): + """Missing docstring checks (Best Practices).""" + + def test_function_without_docstring_flagged(self): + source = """\ + def add(a, b): + return a + b + """ + findings = _analyze_source(source) + self.assertTrue( + any("docstring" in m.lower() for m in _messages(findings)), + "Expected a missing-docstring finding.", + ) + + def test_function_with_docstring_not_flagged(self): + source = """\ + def add(a, b): + \"\"\"Return the sum of a and b.\"\"\" + return a + b + """ + findings = _analyze_source(source) + # Only check function-level docstring findings (exclude file-level) + bp_msgs = [ + f.message for f in findings + if f.category == "Best Practices" + and "docstring" in f.message.lower() + and f.line is not None # file-level findings have line=None + ] + self.assertEqual(bp_msgs, [], "Clean function should not be flagged.") + + def test_private_function_docstring_not_required(self): + source = """\ + def _helper(x): + return x * 2 + """ + findings = _analyze_source(source) + bp_msgs = [ + f.message for f in findings + if "docstring" in f.message.lower() + and f.line is not None # exclude file-level module finding + ] + self.assertEqual(bp_msgs, [], "Private functions should not require a docstring.") + + def test_class_without_docstring_flagged(self): + source = """\ + class MyClass: + pass + """ + findings = _analyze_source(source) + self.assertTrue( + any("docstring" in m.lower() for m in _messages(findings)) + ) + + +class TestLongFunctions(unittest.TestCase): + """Long function detection (Style).""" + + def test_long_function_flagged(self): + # 10 real lines + 1 def + 1 docstring = 12 lines, but we force threshold low. + body = "\n".join(f" x_{i} = {i}" for i in range(10)) + source = f'def long_func():\n """Docstring."""\n{body}\n' + findings = _analyze_source(source, max_function_lines=5) + style_msgs = [f for f in findings if f.category == "Style"] + self.assertTrue( + any("lines long" in m.message for m in style_msgs), + "Expected a 'lines long' style finding.", + ) + + def test_short_function_not_flagged(self): + source = """\ + def short(): + \"\"\"Docstring.\"\"\" + return 1 + """ + findings = _analyze_source(source, max_function_lines=50) + style_msgs = [f for f in findings if f.category == "Style" and "lines long" in f.message] + self.assertEqual(style_msgs, []) + + +class TestTooManyArguments(unittest.TestCase): + """Excessive argument count detection (Best Practices).""" + + def test_too_many_args_flagged(self): + source = """\ + def func(a, b, c, d, e, f): + \"\"\"Six args.\"\"\" + pass + """ + findings = _analyze_source(source, max_function_args=5) + self.assertTrue( + any("arguments" in m.lower() for m in _messages(findings)) + ) + + def test_acceptable_arg_count_not_flagged(self): + source = """\ + def func(a, b, c): + \"\"\"Three args โ€” fine.\"\"\" + pass + """ + findings = _analyze_source(source, max_function_args=5) + self.assertFalse( + any("arguments" in m.lower() for m in _messages(findings)) + ) + + +class TestNestedLoops(unittest.TestCase): + """Nested loop / complexity detection.""" + + def test_nested_for_loops_flagged(self): + source = """\ + def process(data): + \"\"\"Nested loops.\"\"\" + for i in data: + for j in data: + pass + """ + findings = _analyze_source(source) + complexity_msgs = [f for f in findings if f.category == "Complexity"] + self.assertTrue( + any("nested" in m.message.lower() for m in complexity_msgs), + "Expected a nested-loop finding.", + ) + + def test_single_loop_not_flagged(self): + source = """\ + def process(data): + \"\"\"Single loop.\"\"\" + for i in data: + pass + """ + findings = _analyze_source(source) + complexity_msgs = [f for f in findings if f.category == "Complexity"] + self.assertEqual(complexity_msgs, []) + + def test_nested_while_loop_flagged(self): + source = """\ + def process(n): + \"\"\"Nested while.\"\"\" + i = 0 + while i < n: + j = 0 + while j < n: + j += 1 + i += 1 + """ + findings = _analyze_source(source) + complexity_msgs = [f for f in findings if f.category == "Complexity"] + self.assertTrue(len(complexity_msgs) >= 1) + + +class TestWhileLoopWarning(unittest.TestCase): + """While loop termination warnings.""" + + def test_while_loop_info_emitted(self): + source = """\ + def run(): + \"\"\"Has a while loop.\"\"\" + i = 0 + while i < 10: + i += 1 + """ + findings = _analyze_source(source) + bp_msgs = [f for f in findings if f.category == "Best Practices"] + self.assertTrue( + any("while" in m.message.lower() for m in bp_msgs) + ) + + +class TestNamingConvention(unittest.TestCase): + """snake_case convention checks (Style).""" + + def test_camel_case_function_flagged(self): + source = """\ + def myFunction(): + \"\"\"Not snake_case.\"\"\" + pass + """ + findings = _analyze_source(source) + style_msgs = [f for f in findings if f.category == "Style"] + self.assertTrue( + any("snake_case" in m.message for m in style_msgs) + ) + + def test_snake_case_function_not_flagged(self): + source = """\ + def my_function(): + \"\"\"Proper snake_case.\"\"\" + pass + """ + findings = _analyze_source(source) + style_msgs = [ + f for f in findings + if f.category == "Style" and "snake_case" in f.message + ] + self.assertEqual(style_msgs, []) + + +class TestErrorHandling(unittest.TestCase): + """Error handling โ€” FileAnalyzer should raise standard exceptions.""" + + def test_file_not_found(self): + with self.assertRaises(FileNotFoundError): + FileAnalyzer("/nonexistent/path/to/file.py").analyze() + + def test_syntax_error(self): + import tempfile + + with tempfile.NamedTemporaryFile( + mode="w", suffix=".py", delete=False, encoding="utf-8" + ) as tmp: + tmp.write("def broken(\n") + tmp_path = tmp.name + + try: + with self.assertRaises(SyntaxError): + FileAnalyzer(tmp_path).analyze() + finally: + os.unlink(tmp_path) + + +class TestModuleDocstring(unittest.TestCase): + """Module-level docstring check.""" + + def test_module_without_docstring_flagged(self): + source = """\ + x = 1 + """ + findings = _analyze_source(source) + file_level = [f for f in findings if f.line is None] + self.assertTrue(len(file_level) >= 1) + + def test_module_with_docstring_not_flagged(self): + source = """\ + \"\"\"My module docstring.\"\"\" + x = 1 + """ + findings = _analyze_source(source) + file_level = [f for f in findings if f.line is None] + self.assertEqual(file_level, []) + + +# --------------------------------------------------------------------------- +# Runner +# --------------------------------------------------------------------------- + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/smart_code_reviewer/utils.cpython-312.pyc b/smart_code_reviewer/utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..86a868c114840d344ac639e13fb4da080509d787 GIT binary patch literal 12365 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 literal 0 HcmV?d00001 diff --git a/smart_code_reviewer/utils.py b/smart_code_reviewer/utils.py new file mode 100644 index 0000000..bb671e8 --- /dev/null +++ b/smart_code_reviewer/utils.py @@ -0,0 +1,252 @@ +""" +utils.py - Helper utilities for Smart Code Reviewer. + +Contains: + - ANSI colour helpers for terminal output + - JSON serialisation helpers + - Summary statistics builder +""" + +import json +import os +import sys +from typing import Dict, List + +from analyzer import AnalysisResult, Finding + + +# --------------------------------------------------------------------------- +# Terminal colour support +# --------------------------------------------------------------------------- + +# Respect NO_COLOR env var (https://no-color.org/) and non-TTY streams. +_COLOUR_ENABLED: bool = ( + sys.stdout.isatty() + and os.environ.get("NO_COLOR") is None + and os.environ.get("TERM") != "dumb" +) + + +class Colour: + """ANSI escape codes used throughout the reporter.""" + + RESET = "\033[0m" + BOLD = "\033[1m" + DIM = "\033[2m" + + RED = "\033[91m" + YELLOW = "\033[93m" + CYAN = "\033[96m" + GREEN = "\033[92m" + MAGENTA = "\033[95m" + WHITE = "\033[97m" + GREY = "\033[90m" + + @staticmethod + def apply(code: str, text: str) -> str: + """Wrap *text* in *code* if colour output is enabled.""" + if not _COLOUR_ENABLED: + return text + return f"{code}{text}{Colour.RESET}" + + # Convenience shortcuts + @staticmethod + def bold(text: str) -> str: + return Colour.apply(Colour.BOLD, text) + + @staticmethod + def red(text: str) -> str: + return Colour.apply(Colour.RED, text) + + @staticmethod + def yellow(text: str) -> str: + return Colour.apply(Colour.YELLOW, text) + + @staticmethod + def cyan(text: str) -> str: + return Colour.apply(Colour.CYAN, text) + + @staticmethod + def green(text: str) -> str: + return Colour.apply(Colour.GREEN, text) + + @staticmethod + def grey(text: str) -> str: + return Colour.apply(Colour.GREY, text) + + @staticmethod + def magenta(text: str) -> str: + return Colour.apply(Colour.MAGENTA, text) + + +# --------------------------------------------------------------------------- +# Severity helpers +# --------------------------------------------------------------------------- + +_SEVERITY_COLOUR: Dict[str, str] = { + "error": Colour.RED, + "warning": Colour.YELLOW, + "info": Colour.CYAN, +} + +_SEVERITY_ICON: Dict[str, str] = { + "error": "โœ–", + "warning": "โš ", + "info": "โ„น", +} + + +def format_severity(severity: str) -> str: + """Return a coloured, icon-prefixed severity label.""" + icon = _SEVERITY_ICON.get(severity, "ยท") + colour = _SEVERITY_COLOUR.get(severity, "") + label = f"{icon} {severity.upper()}" + return Colour.apply(colour, label) + + +# --------------------------------------------------------------------------- +# Report formatting +# --------------------------------------------------------------------------- + +CATEGORIES = ("Complexity", "Best Practices", "Style") + +_CATEGORY_COLOUR: Dict[str, str] = { + "Complexity": Colour.MAGENTA, + "Best Practices": Colour.CYAN, + "Style": Colour.YELLOW, +} + + +def format_finding(finding: Finding) -> str: + """Format a single :class:`Finding` as a human-readable string.""" + sev_label = format_severity(finding.severity) + loc = ( + Colour.grey(f"line {finding.line}") + if finding.line + else Colour.grey("file-level") + ) + sym = ( + f" {Colour.bold(finding.symbol)}" if finding.symbol else "" + ) + return f" {sev_label:<22} {loc}{sym}\n {finding.message}" + + +def print_report(result: AnalysisResult) -> None: + """Print a full human-readable report for one :class:`AnalysisResult`.""" + # ---- Header ---- + print() + print(Colour.bold("=" * 64)) + print( + Colour.bold(" Smart Code Reviewer") + + " " + + Colour.grey(result.filepath) + ) + print(Colour.bold("=" * 64)) + + # ---- Stats line ---- + stats = ( + f" {Colour.grey('Lines:')} {result.total_lines}" + f" {Colour.grey('Functions:')} {result.total_functions}" + f" {Colour.grey('Classes:')} {result.total_classes}" + ) + print(stats) + + total = len(result.findings) + if total == 0: + print() + print(Colour.green(" โœ” No issues found. Great work!")) + print() + return + + # Summary counts + errors = result.error_count() + warnings = result.warning_count() + infos = result.info_count() + summary_parts = [] + if errors: + summary_parts.append(Colour.red(f"{errors} error{'s' if errors > 1 else ''}")) + if warnings: + summary_parts.append(Colour.yellow(f"{warnings} warning{'s' if warnings > 1 else ''}")) + if infos: + summary_parts.append(Colour.cyan(f"{infos} info")) + + print(f" Found {total} issue{'s' if total > 1 else ''}: {', '.join(summary_parts)}") + + # ---- Category sections ---- + for category in CATEGORIES: + findings = result.by_category(category) + if not findings: + continue + + cat_colour = _CATEGORY_COLOUR.get(category, "") + print() + print(Colour.apply(cat_colour, Colour.bold(f" โ”€โ”€ {category} "))) + print(Colour.apply(cat_colour, " " + "โ”€" * 58)) + for finding in findings: + print(format_finding(finding)) + print() + + # ---- Footer ---- + print(Colour.bold("=" * 64)) + print() + + +def print_multi_summary(results: List[AnalysisResult]) -> None: + """Print a brief aggregate summary when multiple files are reviewed.""" + if len(results) <= 1: + return + + total_issues = sum(len(r.findings) for r in results) + total_errors = sum(r.error_count() for r in results) + total_warns = sum(r.warning_count() for r in results) + + print() + print(Colour.bold("โ•" * 64)) + print(Colour.bold(" Aggregate Summary")) + print(Colour.bold("โ•" * 64)) + print(f" Files analysed : {len(results)}") + print(f" Total issues : {total_issues}") + print( + f" Errors : {Colour.red(str(total_errors))}" + f" Warnings : {Colour.yellow(str(total_warns))}" + ) + clean = sum(1 for r in results if len(r.findings) == 0) + print(f" Clean files : {Colour.green(str(clean))}") + print(Colour.bold("โ•" * 64)) + print() + + +# --------------------------------------------------------------------------- +# JSON serialisation +# --------------------------------------------------------------------------- + +def result_to_dict(result: AnalysisResult) -> dict: + """Convert an :class:`AnalysisResult` to a plain dict (JSON-serialisable).""" + return { + "filepath": result.filepath, + "summary": { + "total_lines": result.total_lines, + "total_functions": result.total_functions, + "total_classes": result.total_classes, + "total_issues": len(result.findings), + "errors": result.error_count(), + "warnings": result.warning_count(), + "infos": result.info_count(), + }, + "findings": [ + { + "category": f.category, + "severity": f.severity, + "line": f.line, + "symbol": f.symbol, + "message": f.message, + } + for f in result.findings + ], + } + + +def print_json(results: List[AnalysisResult]) -> None: + """Serialise results to JSON and write to stdout.""" + payload = [result_to_dict(r) for r in results] + print(json.dumps(payload, indent=2)) From 68fde361c1a3fd29ebd59b0c914c8db0503d4040 Mon Sep 17 00:00:00 2001 From: TheBinaryAVA Date: Wed, 6 May 2026 22:20:01 +0530 Subject: [PATCH 4/4] Delete OSPC CONTRIBUTION directory --- OSPC CONTRIBUTION/README.md | 258 -------------- OSPC CONTRIBUTION/analyzer.cpython-312.pyc | Bin 14295 -> 0 bytes OSPC CONTRIBUTION/analyzer.py | 330 ------------------ OSPC CONTRIBUTION/requirements.txt | 14 - OSPC CONTRIBUTION/reviewer.py | 201 ----------- .../test_analyzer.cpython-312.pyc | Bin 17372 -> 0 bytes OSPC CONTRIBUTION/test_analyzer.py | 313 ----------------- OSPC CONTRIBUTION/utils.cpython-312.pyc | Bin 12365 -> 0 bytes OSPC CONTRIBUTION/utils.py | 252 ------------- 9 files changed, 1368 deletions(-) delete mode 100644 OSPC CONTRIBUTION/README.md delete mode 100644 OSPC CONTRIBUTION/analyzer.cpython-312.pyc delete mode 100644 OSPC CONTRIBUTION/analyzer.py delete mode 100644 OSPC CONTRIBUTION/requirements.txt delete mode 100644 OSPC CONTRIBUTION/reviewer.py delete mode 100644 OSPC CONTRIBUTION/test_analyzer.cpython-312.pyc delete mode 100644 OSPC CONTRIBUTION/test_analyzer.py delete mode 100644 OSPC CONTRIBUTION/utils.cpython-312.pyc delete mode 100644 OSPC CONTRIBUTION/utils.py diff --git a/OSPC CONTRIBUTION/README.md b/OSPC CONTRIBUTION/README.md deleted file mode 100644 index 02186a3..0000000 --- a/OSPC CONTRIBUTION/README.md +++ /dev/null @@ -1,258 +0,0 @@ -# Smart Code Reviewer ๐Ÿ” -BY AVANTHIKA - -A zero-dependency Python static analysis tool that reviews your Python source -files and reports on **code quality**, **time-complexity patterns**, and -**best-practice violations** โ€” all from the command line. - -Built entirely with the Python standard library (`ast`, `argparse`, `json`). - ---- - -## Features - -| Category | Check | -|---|---| -| **Complexity** | Nested loops (possible O(nยฒ)) | -| **Best Practices** | Missing module / class / function docstrings | -| **Best Practices** | `while` loops without obvious termination | -| **Best Practices** | Functions with too many arguments (>5) | -| **Style** | Long functions (>50 lines) | -| **Style** | Non-snake_case function names | - -Additional capabilities: - -- โœ… Analyse **multiple files** in one command -- โœ… **JSON output** mode (`--json`) for CI/CD pipelines -- โœ… **Adjustable thresholds** (`--max-lines`, `--max-args`) -- โœ… Colour output with `NO_COLOR` support -- โœ… Meaningful exit codes for scripting - ---- - -## Requirements - -- Python **3.8+** -- No external packages - ---- - -## Installation - -```bash -# Clone / download the project -git clone https://github.com/yourname/smart-code-reviewer.git -cd smart-code-reviewer - -# (Optional) make the CLI executable -chmod +x reviewer.py -``` - -No `pip install` step required. - ---- - -## Usage - -### Basic review - -```bash -python reviewer.py myfile.py -``` - -### Review multiple files - -```bash -python reviewer.py src/main.py src/utils.py src/models.py -``` - -### Glob expansion (shell feature) - -```bash -python reviewer.py src/*.py -``` - -### JSON output (great for CI) - -```bash -python reviewer.py myfile.py --json -``` - -### Custom thresholds - -```bash -# Flag functions longer than 30 lines, or with more than 4 args -python reviewer.py myfile.py --max-lines 30 --max-args 4 -``` - -### Disable colour (e.g. when piping output) - -```bash -python reviewer.py myfile.py --no-color -# or use the standard env var: -NO_COLOR=1 python reviewer.py myfile.py -``` - -### Full help - -```bash -python reviewer.py --help -``` - ---- - -## Example output - -``` -================================================================ - Smart Code Reviewer examples/sample_bad.py -================================================================ - Lines: 72 Functions: 4 Classes: 1 - Found 8 issues: 2 warnings, 3 warnings, 3 infos - - โ”€โ”€ Complexity โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - โš  WARNING line 18 [process_data] - Nested for-loop detected. This may indicate O(nยฒ) or worse time complexity. - - โ”€โ”€ Best Practices โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - โš  WARNING line 12 [DataLoader] - Class is missing a docstring. - - โš  WARNING line 25 [process_data] - Public function is missing a docstring. - - โš  WARNING line 40 [connect] - Function has 6 arguments (threshold: 5). Consider using a config object or dataclass. - - โ„น INFO line 34 - while-loop found. Ensure the termination condition is guaranteed to prevent infinite loops. - - โ”€โ”€ Style โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - โš  WARNING line 25 [process_data] - Function is 52 lines long (threshold: 50). - - โ„น INFO line 60 [loadData] - Function name 'loadData' does not follow snake_case convention. - -================================================================ -``` - -### JSON output - -```json -[ - { - "filepath": "examples/sample_bad.py", - "summary": { - "total_lines": 72, - "total_functions": 4, - "total_classes": 1, - "total_issues": 7, - "errors": 0, - "warnings": 5, - "infos": 2 - }, - "findings": [ - { - "category": "Complexity", - "severity": "warning", - "line": 18, - "symbol": "process_data", - "message": "Nested for-loop detected. This may indicate O(nยฒ) or worse time complexity." - } - ] - } -] -``` - ---- - -## Running the tests - -```bash -# From the project root -python -m unittest discover -s tests -v -``` - -Expected output: - -``` -test_acceptable_arg_count_not_flagged (test_analyzer.TestTooManyArguments) ... ok -test_camel_case_function_flagged (test_analyzer.TestNamingConvention) ... ok -test_class_without_docstring_flagged (test_analyzer.TestMissingDocstrings) ... ok -... ----------------------------------------------------------------------- -Ran 18 tests in 0.042s - -OK -``` - ---- - -## Project structure - -``` -smart-code-reviewer/ -โ”œโ”€โ”€ reviewer.py # CLI entry point (argparse, exit codes) -โ”œโ”€โ”€ analyzer.py # Core AST visitor + data model -โ”œโ”€โ”€ utils.py # Colour output, report formatting, JSON helpers -โ”œโ”€โ”€ requirements.txt # No external deps โ€” standard library only -โ”œโ”€โ”€ README.md -โ””โ”€โ”€ tests/ - โ””โ”€โ”€ test_analyzer.py # 18 unit tests covering all checks -``` - ---- - -## Exit codes - -| Code | Meaning | -|---|---| -| `0` | No issues found | -| `1` | Warnings / info found | -| `2` | Errors found | -| `3` | File read / parse failure | -| `4` | Bad CLI arguments | - -Useful for CI pipelines: - -```bash -python reviewer.py src/main.py || echo "Review failed" -``` - ---- - -## Configuration reference - -| Flag | Default | Description | -|---|---|---| -| `--json` | off | Output results as JSON | -| `--no-color` | off | Disable ANSI colour | -| `--max-lines N` | 50 | Max lines per function | -| `--max-args N` | 5 | Max function arguments | - ---- - -## Extending the tool - -All analysis logic lives in `analyzer.py` as an `ast.NodeVisitor` subclass. -To add a new check: - -1. Add a `visit_` method to `CodeAnalyzerVisitor`. -2. Call `self._add(category, severity, line, message)` to record a finding. -3. Add a corresponding unit test in `tests/test_analyzer.py`. - ---- - -## Contributing - -Pull requests are welcome. Please: -- Follow PEP 8 -- Add tests for any new check -- Keep zero external dependencies - ---- - -## License - -MIT ยฉ 2026 diff --git a/OSPC CONTRIBUTION/analyzer.cpython-312.pyc b/OSPC CONTRIBUTION/analyzer.cpython-312.pyc deleted file mode 100644 index aa96f7ce251b85d863d9d94848e1962ee6dfe1a8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14295 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 str: - loc = f"line {self.line}" if self.line else "file-level" - sym = f" [{self.symbol}]" if self.symbol else "" - return f" [{self.severity.upper():7s}] {loc}{sym}: {self.message}" - - -@dataclass -class AnalysisResult: - """Aggregated results for a single file.""" - - filepath: str - findings: List[Finding] = field(default_factory=list) - total_functions: int = 0 - total_classes: int = 0 - total_lines: int = 0 - - # Convenience helpers - def by_category(self, category: str) -> List[Finding]: - return [f for f in self.findings if f.category == category] - - def error_count(self) -> int: - return sum(1 for f in self.findings if f.severity == "error") - - def warning_count(self) -> int: - return sum(1 for f in self.findings if f.severity == "warning") - - def info_count(self) -> int: - return sum(1 for f in self.findings if f.severity == "info") - - -# --------------------------------------------------------------------------- -# Configuration (thresholds) -# --------------------------------------------------------------------------- - -class ReviewConfig: - """Centralised thresholds so they're easy to tune.""" - - MAX_FUNCTION_LINES: int = 50 - MAX_FUNCTION_ARGS: int = 5 - CATEGORIES = ("Complexity", "Best Practices", "Style") - - -# --------------------------------------------------------------------------- -# Visitor -# --------------------------------------------------------------------------- - -class CodeAnalyzerVisitor(ast.NodeVisitor): - """ - Walks an AST and accumulates findings. - - Call `.visit(tree)` then read `.findings`, `.func_count`, `.class_count`. - """ - - def __init__(self, source_lines: List[str], config: ReviewConfig): - self._lines = source_lines - self._cfg = config - self.findings: List[Finding] = [] - self.func_count: int = 0 - self.class_count: int = 0 - - # Tracks nesting depth so we can flag nested loops. - self._loop_depth: int = 0 - - # ------------------------------------------------------------------ - # Internal helpers - # ------------------------------------------------------------------ - - def _add( - self, - category: str, - severity: str, - line: Optional[int], - message: str, - symbol: str = "", - ) -> None: - self.findings.append( - Finding( - category=category, - severity=severity, - line=line, - message=message, - symbol=symbol, - ) - ) - - def _function_line_count(self, node: ast.FunctionDef) -> int: - """Return the number of source lines spanned by a function node.""" - return node.end_lineno - node.lineno + 1 # type: ignore[attr-defined] - - def _has_docstring(self, node: ast.AST) -> bool: - """Return True if the first statement is a string literal (docstring).""" - body = getattr(node, "body", []) - if body and isinstance(body[0], ast.Expr): - val = body[0].value - return isinstance(val, ast.Constant) and isinstance(val.value, str) - return False - - # ------------------------------------------------------------------ - # Visitors - # ------------------------------------------------------------------ - - def visit_FunctionDef(self, node: ast.FunctionDef) -> None: # noqa: N802 - self._check_function(node) - self.generic_visit(node) - - # async defs get the same treatment - visit_AsyncFunctionDef = visit_FunctionDef # type: ignore[assignment] - - def visit_ClassDef(self, node: ast.ClassDef) -> None: # noqa: N802 - self.class_count += 1 - if not self._has_docstring(node): - self._add( - "Best Practices", - "warning", - node.lineno, - "Class is missing a docstring.", - symbol=node.name, - ) - self.generic_visit(node) - - def visit_For(self, node: ast.For) -> None: # noqa: N802 - self._check_loop(node, loop_type="for") - - def visit_While(self, node: ast.While) -> None: # noqa: N802 - self._check_loop(node, loop_type="while") - - # ------------------------------------------------------------------ - # Check helpers - # ------------------------------------------------------------------ - - def _check_function(self, node: ast.FunctionDef) -> None: - self.func_count += 1 - name = node.name - - # --- Style: long function --- - line_count = self._function_line_count(node) - if line_count > self._cfg.MAX_FUNCTION_LINES: - self._add( - "Style", - "warning", - node.lineno, - f"Function is {line_count} lines long " - f"(threshold: {self._cfg.MAX_FUNCTION_LINES}).", - symbol=name, - ) - - # --- Best Practices: missing docstring --- - if not self._has_docstring(node) and not name.startswith("_"): - self._add( - "Best Practices", - "warning", - node.lineno, - "Public function is missing a docstring.", - symbol=name, - ) - - # --- Best Practices: too many arguments --- - n_args = len(node.args.args) - if n_args > self._cfg.MAX_FUNCTION_ARGS: - self._add( - "Best Practices", - "warning", - node.lineno, - f"Function has {n_args} arguments " - f"(threshold: {self._cfg.MAX_FUNCTION_ARGS}). " - "Consider using a config object or dataclass.", - symbol=name, - ) - - # --- Style: naming convention (should be snake_case) --- - if not _is_snake_case(name) and not name.startswith("__"): - self._add( - "Style", - "info", - node.lineno, - f"Function name '{name}' does not follow snake_case convention.", - symbol=name, - ) - - def _check_loop(self, node: ast.AST, loop_type: str) -> None: - if self._loop_depth > 0: - # We're already inside a loop โ€” this is a nested loop. - self._add( - "Complexity", - "warning", - node.lineno, # type: ignore[attr-defined] - f"Nested {loop_type}-loop detected. " - "This may indicate O(nยฒ) or worse time complexity.", - ) - - if loop_type == "while": - self._add( - "Best Practices", - "info", - node.lineno, # type: ignore[attr-defined] - "while-loop found. Ensure the termination condition is " - "guaranteed to prevent infinite loops.", - ) - - # Recurse with incremented depth. - self._loop_depth += 1 - self.generic_visit(node) - self._loop_depth -= 1 - - -# --------------------------------------------------------------------------- -# Module-level checks (run on the whole tree, not per-node) -# --------------------------------------------------------------------------- - -def _check_module_docstring(tree: ast.Module) -> Optional[Finding]: - """Return a finding if the module lacks a module-level docstring.""" - body = tree.body - if body and isinstance(body[0], ast.Expr): - val = body[0].value - if isinstance(val, ast.Constant) and isinstance(val.value, str): - return None # has docstring - return Finding( - category="Best Practices", - severity="info", - line=None, - message="Module is missing a module-level docstring.", - ) - - -# --------------------------------------------------------------------------- -# Utility -# --------------------------------------------------------------------------- - -def _is_snake_case(name: str) -> bool: - """ - Return True when *name* looks like valid Python snake_case. - - Dunder methods like __init__ are excluded by the caller. - """ - return name == name.lower() and not name[0].isdigit() - - -# --------------------------------------------------------------------------- -# Public API -# --------------------------------------------------------------------------- - -class FileAnalyzer: - """ - High-level interface: parse a file and return an :class:`AnalysisResult`. - - Example:: - - result = FileAnalyzer("mymodule.py").analyze() - for finding in result.findings: - print(finding) - """ - - def __init__(self, filepath: str, config: Optional[ReviewConfig] = None): - self.filepath = filepath - self.config = config or ReviewConfig() - - def analyze(self) -> AnalysisResult: - """ - Read, parse, and analyse the target file. - - Raises: - FileNotFoundError: if the file path does not exist. - SyntaxError: if the file contains invalid Python syntax. - OSError: for other I/O related errors. - """ - source = self._read_source() - tree = self._parse(source) - source_lines = source.splitlines() - - result = AnalysisResult( - filepath=self.filepath, - total_lines=len(source_lines), - ) - - # Module-level docstring check - mod_finding = _check_module_docstring(tree) - if mod_finding: - result.findings.append(mod_finding) - - # Walk AST - visitor = CodeAnalyzerVisitor(source_lines, self.config) - visitor.visit(tree) - - result.findings.extend(visitor.findings) - result.total_functions = visitor.func_count - result.total_classes = visitor.class_count - - return result - - # ------------------------------------------------------------------ - # Private helpers - # ------------------------------------------------------------------ - - def _read_source(self) -> str: - """Read the file and return its content as a string.""" - with open(self.filepath, "r", encoding="utf-8") as fh: - return fh.read() - - def _parse(self, source: str) -> ast.Module: - """Parse source into an AST, raising SyntaxError on failure.""" - return ast.parse(source, filename=self.filepath) diff --git a/OSPC CONTRIBUTION/requirements.txt b/OSPC CONTRIBUTION/requirements.txt deleted file mode 100644 index b0d40e9..0000000 --- a/OSPC CONTRIBUTION/requirements.txt +++ /dev/null @@ -1,14 +0,0 @@ -# Smart Code Reviewer โ€” Python dependencies -# -# This tool uses ONLY the Python standard library. -# No third-party packages are required. -# -# Minimum Python version: 3.8 -# (ast.FunctionDef.end_lineno was added in 3.8) -# -# To run tests: -# python -m unittest discover -s tests -# -# Optional (for development / linting): -# pycodestyle>=2.11 -# mypy>=1.9 diff --git a/OSPC CONTRIBUTION/reviewer.py b/OSPC CONTRIBUTION/reviewer.py deleted file mode 100644 index ed73a67..0000000 --- a/OSPC CONTRIBUTION/reviewer.py +++ /dev/null @@ -1,201 +0,0 @@ -#!/usr/bin/env python3 -""" -reviewer.py - CLI entry point for Smart Code Reviewer. - -Usage examples: - python reviewer.py myfile.py - python reviewer.py src/main.py src/utils.py - python reviewer.py *.py --json - python reviewer.py myfile.py --no-color -""" - -import argparse -import os -import sys - -# Allow running from any working directory (add package root to path). -sys.path.insert(0, os.path.dirname(__file__)) - -from analyzer import FileAnalyzer, ReviewConfig -from utils import print_json, print_multi_summary, print_report - - -# --------------------------------------------------------------------------- -# Exit codes (follow UNIX conventions) -# --------------------------------------------------------------------------- - -EXIT_OK = 0 # No issues found -EXIT_ISSUES = 1 # Issues found (warnings / infos) -EXIT_ERRORS = 2 # Hard errors found -EXIT_IO_ERROR = 3 # File read / parse failure -EXIT_USAGE_ERROR = 4 # Bad CLI arguments - - -# --------------------------------------------------------------------------- -# CLI definition -# --------------------------------------------------------------------------- - -def build_parser() -> argparse.ArgumentParser: - """Return the configured argument parser.""" - parser = argparse.ArgumentParser( - prog="reviewer", - description=( - "Smart Code Reviewer โ€” static analysis tool for Python files.\n" - "Analyses code quality, time complexity heuristics, and best-practice violations." - ), - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=""" -examples: - python reviewer.py myfile.py - python reviewer.py src/a.py src/b.py - python reviewer.py myfile.py --json - python reviewer.py myfile.py --max-lines 30 --max-args 4 - """, - ) - - parser.add_argument( - "files", - metavar="FILE", - nargs="+", - help="One or more Python source files to analyse.", - ) - - parser.add_argument( - "--json", - action="store_true", - default=False, - help="Output results as JSON instead of the human-readable report.", - ) - - parser.add_argument( - "--no-color", - action="store_true", - default=False, - help="Disable ANSI colour output.", - ) - - parser.add_argument( - "--max-lines", - metavar="N", - type=int, - default=ReviewConfig.MAX_FUNCTION_LINES, - help=( - f"Maximum lines per function before a style warning is raised " - f"(default: {ReviewConfig.MAX_FUNCTION_LINES})." - ), - ) - - parser.add_argument( - "--max-args", - metavar="N", - type=int, - default=ReviewConfig.MAX_FUNCTION_ARGS, - help=( - f"Maximum function arguments before a best-practice warning is raised " - f"(default: {ReviewConfig.MAX_FUNCTION_ARGS})." - ), - ) - - return parser - - -# --------------------------------------------------------------------------- -# Core runner -# --------------------------------------------------------------------------- - -def run(args: argparse.Namespace) -> int: - """ - Execute the review for all specified files. - - Returns an exit code (see module-level constants). - """ - # Honour --no-color by patching the environment variable that utils.py - # respects without needing to pass state through function arguments. - if args.no_color: - os.environ["NO_COLOR"] = "1" - - # Build shared config from CLI flags. - config = ReviewConfig() - config.MAX_FUNCTION_LINES = args.max_lines - config.MAX_FUNCTION_ARGS = args.max_args - - results = [] - had_io_error = False - - for filepath in args.files: - # Validate extension (allow but warn on non-.py files). - if not filepath.endswith(".py"): - _warn(f"'{filepath}' does not appear to be a Python file. Skipping.") - continue - - try: - analyzer = FileAnalyzer(filepath, config=config) - result = analyzer.analyze() - results.append(result) - - except FileNotFoundError: - _error(f"File not found: {filepath}") - had_io_error = True - - except SyntaxError as exc: - _error( - f"Syntax error in '{filepath}' at line {exc.lineno}: {exc.msg}" - ) - had_io_error = True - - except OSError as exc: - _error(f"Cannot read '{filepath}': {exc.strerror}") - had_io_error = True - - if not results: - _error("No files were successfully analysed.") - return EXIT_IO_ERROR if had_io_error else EXIT_USAGE_ERROR - - # ---- Output ---- - if args.json: - print_json(results) - else: - for result in results: - print_report(result) - if len(results) > 1: - print_multi_summary(results) - - # ---- Determine exit code ---- - if had_io_error: - return EXIT_IO_ERROR - - total_errors = sum(r.error_count() for r in results) - if total_errors: - return EXIT_ERRORS - - total_issues = sum(len(r.findings) for r in results) - if total_issues: - return EXIT_ISSUES - - return EXIT_OK - - -# --------------------------------------------------------------------------- -# Output helpers -# --------------------------------------------------------------------------- - -def _warn(message: str) -> None: - print(f"[WARNING] {message}", file=sys.stderr) - - -def _error(message: str) -> None: - print(f"[ERROR] {message}", file=sys.stderr) - - -# --------------------------------------------------------------------------- -# Entry point -# --------------------------------------------------------------------------- - -def main() -> None: - parser = build_parser() - args = parser.parse_args() - sys.exit(run(args)) - - -if __name__ == "__main__": - main() diff --git a/OSPC CONTRIBUTION/test_analyzer.cpython-312.pyc b/OSPC CONTRIBUTION/test_analyzer.cpython-312.pyc deleted file mode 100644 index 6eaa4c5d550ecf23123a3602e8df0eca6aaad625..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17372 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 diff --git a/OSPC CONTRIBUTION/test_analyzer.py b/OSPC CONTRIBUTION/test_analyzer.py deleted file mode 100644 index 7450df0..0000000 --- a/OSPC CONTRIBUTION/test_analyzer.py +++ /dev/null @@ -1,313 +0,0 @@ -""" -tests/test_analyzer.py - Unit tests for Smart Code Reviewer. - -Run with: - python -m unittest discover -s tests - # or directly: - python tests/test_analyzer.py -""" - -import os -import sys -import textwrap -import unittest - -# Ensure the package root is importable regardless of where tests are run. -sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) - -from analyzer import FileAnalyzer, ReviewConfig - - -# --------------------------------------------------------------------------- -# Helper -# --------------------------------------------------------------------------- - -def _analyze_source(source: str, **config_kwargs) -> list: - """ - Parse *source* from a string and return the list of findings. - - Writes a temporary file because :class:`FileAnalyzer` works on paths. - Uses a fixed temporary file name that is cleaned up after each test. - """ - import tempfile - - config = ReviewConfig() - for key, value in config_kwargs.items(): - setattr(config, key.upper(), value) - - with tempfile.NamedTemporaryFile( - mode="w", suffix=".py", delete=False, encoding="utf-8" - ) as tmp: - tmp.write(textwrap.dedent(source)) - tmp_path = tmp.name - - try: - result = FileAnalyzer(tmp_path, config=config).analyze() - return result.findings - finally: - os.unlink(tmp_path) - - -def _categories(findings) -> list: - return [f.category for f in findings] - - -def _messages(findings) -> list: - return [f.message for f in findings] - - -def _severities(findings) -> list: - return [f.severity for f in findings] - - -# --------------------------------------------------------------------------- -# Test cases -# --------------------------------------------------------------------------- - -class TestMissingDocstrings(unittest.TestCase): - """Missing docstring checks (Best Practices).""" - - def test_function_without_docstring_flagged(self): - source = """\ - def add(a, b): - return a + b - """ - findings = _analyze_source(source) - self.assertTrue( - any("docstring" in m.lower() for m in _messages(findings)), - "Expected a missing-docstring finding.", - ) - - def test_function_with_docstring_not_flagged(self): - source = """\ - def add(a, b): - \"\"\"Return the sum of a and b.\"\"\" - return a + b - """ - findings = _analyze_source(source) - # Only check function-level docstring findings (exclude file-level) - bp_msgs = [ - f.message for f in findings - if f.category == "Best Practices" - and "docstring" in f.message.lower() - and f.line is not None # file-level findings have line=None - ] - self.assertEqual(bp_msgs, [], "Clean function should not be flagged.") - - def test_private_function_docstring_not_required(self): - source = """\ - def _helper(x): - return x * 2 - """ - findings = _analyze_source(source) - bp_msgs = [ - f.message for f in findings - if "docstring" in f.message.lower() - and f.line is not None # exclude file-level module finding - ] - self.assertEqual(bp_msgs, [], "Private functions should not require a docstring.") - - def test_class_without_docstring_flagged(self): - source = """\ - class MyClass: - pass - """ - findings = _analyze_source(source) - self.assertTrue( - any("docstring" in m.lower() for m in _messages(findings)) - ) - - -class TestLongFunctions(unittest.TestCase): - """Long function detection (Style).""" - - def test_long_function_flagged(self): - # 10 real lines + 1 def + 1 docstring = 12 lines, but we force threshold low. - body = "\n".join(f" x_{i} = {i}" for i in range(10)) - source = f'def long_func():\n """Docstring."""\n{body}\n' - findings = _analyze_source(source, max_function_lines=5) - style_msgs = [f for f in findings if f.category == "Style"] - self.assertTrue( - any("lines long" in m.message for m in style_msgs), - "Expected a 'lines long' style finding.", - ) - - def test_short_function_not_flagged(self): - source = """\ - def short(): - \"\"\"Docstring.\"\"\" - return 1 - """ - findings = _analyze_source(source, max_function_lines=50) - style_msgs = [f for f in findings if f.category == "Style" and "lines long" in f.message] - self.assertEqual(style_msgs, []) - - -class TestTooManyArguments(unittest.TestCase): - """Excessive argument count detection (Best Practices).""" - - def test_too_many_args_flagged(self): - source = """\ - def func(a, b, c, d, e, f): - \"\"\"Six args.\"\"\" - pass - """ - findings = _analyze_source(source, max_function_args=5) - self.assertTrue( - any("arguments" in m.lower() for m in _messages(findings)) - ) - - def test_acceptable_arg_count_not_flagged(self): - source = """\ - def func(a, b, c): - \"\"\"Three args โ€” fine.\"\"\" - pass - """ - findings = _analyze_source(source, max_function_args=5) - self.assertFalse( - any("arguments" in m.lower() for m in _messages(findings)) - ) - - -class TestNestedLoops(unittest.TestCase): - """Nested loop / complexity detection.""" - - def test_nested_for_loops_flagged(self): - source = """\ - def process(data): - \"\"\"Nested loops.\"\"\" - for i in data: - for j in data: - pass - """ - findings = _analyze_source(source) - complexity_msgs = [f for f in findings if f.category == "Complexity"] - self.assertTrue( - any("nested" in m.message.lower() for m in complexity_msgs), - "Expected a nested-loop finding.", - ) - - def test_single_loop_not_flagged(self): - source = """\ - def process(data): - \"\"\"Single loop.\"\"\" - for i in data: - pass - """ - findings = _analyze_source(source) - complexity_msgs = [f for f in findings if f.category == "Complexity"] - self.assertEqual(complexity_msgs, []) - - def test_nested_while_loop_flagged(self): - source = """\ - def process(n): - \"\"\"Nested while.\"\"\" - i = 0 - while i < n: - j = 0 - while j < n: - j += 1 - i += 1 - """ - findings = _analyze_source(source) - complexity_msgs = [f for f in findings if f.category == "Complexity"] - self.assertTrue(len(complexity_msgs) >= 1) - - -class TestWhileLoopWarning(unittest.TestCase): - """While loop termination warnings.""" - - def test_while_loop_info_emitted(self): - source = """\ - def run(): - \"\"\"Has a while loop.\"\"\" - i = 0 - while i < 10: - i += 1 - """ - findings = _analyze_source(source) - bp_msgs = [f for f in findings if f.category == "Best Practices"] - self.assertTrue( - any("while" in m.message.lower() for m in bp_msgs) - ) - - -class TestNamingConvention(unittest.TestCase): - """snake_case convention checks (Style).""" - - def test_camel_case_function_flagged(self): - source = """\ - def myFunction(): - \"\"\"Not snake_case.\"\"\" - pass - """ - findings = _analyze_source(source) - style_msgs = [f for f in findings if f.category == "Style"] - self.assertTrue( - any("snake_case" in m.message for m in style_msgs) - ) - - def test_snake_case_function_not_flagged(self): - source = """\ - def my_function(): - \"\"\"Proper snake_case.\"\"\" - pass - """ - findings = _analyze_source(source) - style_msgs = [ - f for f in findings - if f.category == "Style" and "snake_case" in f.message - ] - self.assertEqual(style_msgs, []) - - -class TestErrorHandling(unittest.TestCase): - """Error handling โ€” FileAnalyzer should raise standard exceptions.""" - - def test_file_not_found(self): - with self.assertRaises(FileNotFoundError): - FileAnalyzer("/nonexistent/path/to/file.py").analyze() - - def test_syntax_error(self): - import tempfile - - with tempfile.NamedTemporaryFile( - mode="w", suffix=".py", delete=False, encoding="utf-8" - ) as tmp: - tmp.write("def broken(\n") - tmp_path = tmp.name - - try: - with self.assertRaises(SyntaxError): - FileAnalyzer(tmp_path).analyze() - finally: - os.unlink(tmp_path) - - -class TestModuleDocstring(unittest.TestCase): - """Module-level docstring check.""" - - def test_module_without_docstring_flagged(self): - source = """\ - x = 1 - """ - findings = _analyze_source(source) - file_level = [f for f in findings if f.line is None] - self.assertTrue(len(file_level) >= 1) - - def test_module_with_docstring_not_flagged(self): - source = """\ - \"\"\"My module docstring.\"\"\" - x = 1 - """ - findings = _analyze_source(source) - file_level = [f for f in findings if f.line is None] - self.assertEqual(file_level, []) - - -# --------------------------------------------------------------------------- -# Runner -# --------------------------------------------------------------------------- - -if __name__ == "__main__": - unittest.main(verbosity=2) diff --git a/OSPC CONTRIBUTION/utils.cpython-312.pyc b/OSPC CONTRIBUTION/utils.cpython-312.pyc deleted file mode 100644 index 86a868c114840d344ac639e13fb4da080509d787..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12365 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 diff --git a/OSPC CONTRIBUTION/utils.py b/OSPC CONTRIBUTION/utils.py deleted file mode 100644 index bb671e8..0000000 --- a/OSPC CONTRIBUTION/utils.py +++ /dev/null @@ -1,252 +0,0 @@ -""" -utils.py - Helper utilities for Smart Code Reviewer. - -Contains: - - ANSI colour helpers for terminal output - - JSON serialisation helpers - - Summary statistics builder -""" - -import json -import os -import sys -from typing import Dict, List - -from analyzer import AnalysisResult, Finding - - -# --------------------------------------------------------------------------- -# Terminal colour support -# --------------------------------------------------------------------------- - -# Respect NO_COLOR env var (https://no-color.org/) and non-TTY streams. -_COLOUR_ENABLED: bool = ( - sys.stdout.isatty() - and os.environ.get("NO_COLOR") is None - and os.environ.get("TERM") != "dumb" -) - - -class Colour: - """ANSI escape codes used throughout the reporter.""" - - RESET = "\033[0m" - BOLD = "\033[1m" - DIM = "\033[2m" - - RED = "\033[91m" - YELLOW = "\033[93m" - CYAN = "\033[96m" - GREEN = "\033[92m" - MAGENTA = "\033[95m" - WHITE = "\033[97m" - GREY = "\033[90m" - - @staticmethod - def apply(code: str, text: str) -> str: - """Wrap *text* in *code* if colour output is enabled.""" - if not _COLOUR_ENABLED: - return text - return f"{code}{text}{Colour.RESET}" - - # Convenience shortcuts - @staticmethod - def bold(text: str) -> str: - return Colour.apply(Colour.BOLD, text) - - @staticmethod - def red(text: str) -> str: - return Colour.apply(Colour.RED, text) - - @staticmethod - def yellow(text: str) -> str: - return Colour.apply(Colour.YELLOW, text) - - @staticmethod - def cyan(text: str) -> str: - return Colour.apply(Colour.CYAN, text) - - @staticmethod - def green(text: str) -> str: - return Colour.apply(Colour.GREEN, text) - - @staticmethod - def grey(text: str) -> str: - return Colour.apply(Colour.GREY, text) - - @staticmethod - def magenta(text: str) -> str: - return Colour.apply(Colour.MAGENTA, text) - - -# --------------------------------------------------------------------------- -# Severity helpers -# --------------------------------------------------------------------------- - -_SEVERITY_COLOUR: Dict[str, str] = { - "error": Colour.RED, - "warning": Colour.YELLOW, - "info": Colour.CYAN, -} - -_SEVERITY_ICON: Dict[str, str] = { - "error": "โœ–", - "warning": "โš ", - "info": "โ„น", -} - - -def format_severity(severity: str) -> str: - """Return a coloured, icon-prefixed severity label.""" - icon = _SEVERITY_ICON.get(severity, "ยท") - colour = _SEVERITY_COLOUR.get(severity, "") - label = f"{icon} {severity.upper()}" - return Colour.apply(colour, label) - - -# --------------------------------------------------------------------------- -# Report formatting -# --------------------------------------------------------------------------- - -CATEGORIES = ("Complexity", "Best Practices", "Style") - -_CATEGORY_COLOUR: Dict[str, str] = { - "Complexity": Colour.MAGENTA, - "Best Practices": Colour.CYAN, - "Style": Colour.YELLOW, -} - - -def format_finding(finding: Finding) -> str: - """Format a single :class:`Finding` as a human-readable string.""" - sev_label = format_severity(finding.severity) - loc = ( - Colour.grey(f"line {finding.line}") - if finding.line - else Colour.grey("file-level") - ) - sym = ( - f" {Colour.bold(finding.symbol)}" if finding.symbol else "" - ) - return f" {sev_label:<22} {loc}{sym}\n {finding.message}" - - -def print_report(result: AnalysisResult) -> None: - """Print a full human-readable report for one :class:`AnalysisResult`.""" - # ---- Header ---- - print() - print(Colour.bold("=" * 64)) - print( - Colour.bold(" Smart Code Reviewer") - + " " - + Colour.grey(result.filepath) - ) - print(Colour.bold("=" * 64)) - - # ---- Stats line ---- - stats = ( - f" {Colour.grey('Lines:')} {result.total_lines}" - f" {Colour.grey('Functions:')} {result.total_functions}" - f" {Colour.grey('Classes:')} {result.total_classes}" - ) - print(stats) - - total = len(result.findings) - if total == 0: - print() - print(Colour.green(" โœ” No issues found. Great work!")) - print() - return - - # Summary counts - errors = result.error_count() - warnings = result.warning_count() - infos = result.info_count() - summary_parts = [] - if errors: - summary_parts.append(Colour.red(f"{errors} error{'s' if errors > 1 else ''}")) - if warnings: - summary_parts.append(Colour.yellow(f"{warnings} warning{'s' if warnings > 1 else ''}")) - if infos: - summary_parts.append(Colour.cyan(f"{infos} info")) - - print(f" Found {total} issue{'s' if total > 1 else ''}: {', '.join(summary_parts)}") - - # ---- Category sections ---- - for category in CATEGORIES: - findings = result.by_category(category) - if not findings: - continue - - cat_colour = _CATEGORY_COLOUR.get(category, "") - print() - print(Colour.apply(cat_colour, Colour.bold(f" โ”€โ”€ {category} "))) - print(Colour.apply(cat_colour, " " + "โ”€" * 58)) - for finding in findings: - print(format_finding(finding)) - print() - - # ---- Footer ---- - print(Colour.bold("=" * 64)) - print() - - -def print_multi_summary(results: List[AnalysisResult]) -> None: - """Print a brief aggregate summary when multiple files are reviewed.""" - if len(results) <= 1: - return - - total_issues = sum(len(r.findings) for r in results) - total_errors = sum(r.error_count() for r in results) - total_warns = sum(r.warning_count() for r in results) - - print() - print(Colour.bold("โ•" * 64)) - print(Colour.bold(" Aggregate Summary")) - print(Colour.bold("โ•" * 64)) - print(f" Files analysed : {len(results)}") - print(f" Total issues : {total_issues}") - print( - f" Errors : {Colour.red(str(total_errors))}" - f" Warnings : {Colour.yellow(str(total_warns))}" - ) - clean = sum(1 for r in results if len(r.findings) == 0) - print(f" Clean files : {Colour.green(str(clean))}") - print(Colour.bold("โ•" * 64)) - print() - - -# --------------------------------------------------------------------------- -# JSON serialisation -# --------------------------------------------------------------------------- - -def result_to_dict(result: AnalysisResult) -> dict: - """Convert an :class:`AnalysisResult` to a plain dict (JSON-serialisable).""" - return { - "filepath": result.filepath, - "summary": { - "total_lines": result.total_lines, - "total_functions": result.total_functions, - "total_classes": result.total_classes, - "total_issues": len(result.findings), - "errors": result.error_count(), - "warnings": result.warning_count(), - "infos": result.info_count(), - }, - "findings": [ - { - "category": f.category, - "severity": f.severity, - "line": f.line, - "symbol": f.symbol, - "message": f.message, - } - for f in result.findings - ], - } - - -def print_json(results: List[AnalysisResult]) -> None: - """Serialise results to JSON and write to stdout.""" - payload = [result_to_dict(r) for r in results] - print(json.dumps(payload, indent=2))