oct.linter.oct_lint module#

Purpose#

Provide a strict, deterministic linter for Option C projects that validates mandatory header blocks, module docstrings, diagnostics profiles, and basic testing presence, and can optionally auto-fix header blocks.

Responsibilities#

  • Walk the project tree from a given Option C project root or directory or single file given as an argument.

  • Skip excluded directories and the linter’s own files.

  • Validate the 4-line mandatory header block for every Python module.

  • Validate the presence and structure of the module docstring.

  • Validate the presence of a Diagnostics profile block (Domain + L2/L3/L4).

  • Validate basic _dbg usage in non-trivial modules; _dbg is the structured debug logger provided by the oc_diagnostics package (from oc_diagnostics import _dbg).

  • Validate the presence of regression tests for each module.

  • Optionally auto-fix header blocks while archiving originals.

  • Produce a detailed log file and a colorized terminal summary.

  • Optionally emit machine-readable JSON to stdout (--json).

Diagnostics#

Domain: OCT-LINTER Levels:

L2 — lifecycle L3 — semantic details L4 — deep tracing

Contracts#

  • Must not modify files unless --fix-headers is explicitly requested.

  • Must treat the provided project root as authoritative for all relative paths.

  • Must write a deterministic log file into the project’s logs/ directory when writable; warns and continues if the log directory is unwritable.

  • Must not import project modules; analysis is purely static.

  • Must be callable both from the OCT CLI and as a standalone module.

Dependencies#

  • oct.core.exclusions (directory and wildcard exclusion lists)

  • oct.core.parsing (safe AST parsing helper)

  • oct.core.paths (project-relative path helper)

  • oct.core.waivers (inline waiver parsing)

class oct.linter.oct_lint.LinterContext(project_root: Path, project_name: str, diagnostics_dir: Path, tests_dir: Path, docs_dir: Path, log_handle: Any = None, log_path: Path | None = None, log_lock: lock = <factory>, test_index: dict[str, set[str]] | None=None, secret_match_mode: str = 'substring', code_entropy_threshold: float = 4.5, docstring_entropy_threshold: float = 5.0, comment_entropy_threshold: float = 5.0, entropy_exclusions: list = <factory>, excluded_field_patterns: list = <factory>, profile_name: str = 'strict', lint_cache: Any = None, no_cache_read: bool = False)[source]#

Bases: object

Immutable per-run context for the linter. Replaces module-level globals so that concurrent linter invocations do not share mutable state.

code_entropy_threshold: float = 4.5#
comment_entropy_threshold: float = 5.0#
diagnostics_dir: Path#
docs_dir: Path#
docstring_entropy_threshold: float = 5.0#
entropy_exclusions: list#
excluded_field_patterns: list#
lint_cache: Any = None#
log_handle: Any = None#
log_lock: lock#
log_path: Path | None = None#
no_cache_read: bool = False#
profile_name: str = 'strict'#
project_name: str#
project_root: Path#
secret_match_mode: str = 'substring'#
test_index: dict[str, set[str]] | None = None#
tests_dir: Path#
oct.linter.oct_lint.build_test_index(ctx: LinterContext) dict[str, set[str]][source]#

OI-510 / FS-516: one-shot scan of ctx.tests_dir producing two lookup sets so check_tests_exist() becomes O(1) per module instead of re-walking the tests tree for every file linted.

The index is cached on LinterContext.test_index — subsequent calls return the cached value directly.

Returns a dict with:

  • name_stems — stems <s> for which test_<s>.py exists.

  • imported_stems — module stems referenced in any test file by a whole-word import / from statement.

oct.linter.oct_lint.check_dbg_assert_safety(text: str, tree: Module | None = None, filename: str = '') tuple[bool, str][source]#

Flag _dbg_assert calls made inside security-sensitive contexts.

_dbg_assert is a diagnostic aid — in production mode it is a no-op. Per AP-16 it must never be used for safety invariants (authentication, authorization, input validation, crypto, etc.). This check walks the AST looking for calls named _dbg_assert (bare or attribute access) and verifies that none of the enclosing functions, classes, or the module’s filename stem contain a security-sensitive keyword.

oct.linter.oct_lint.check_dbg_import(text: str, tree: Module | None = None)[source]#

Verify that _dbg is imported from oc_diagnostics at module level.

The canonical import is from oc_diagnostics import _dbg. Files shorter than 20 lines are exempt (same threshold as check_dbg_usage). Aliased imports (import _dbg as ...) are not accepted — the Option C spec requires the canonical form.

oct.linter.oct_lint.check_dbg_usage(text: str, tree: Module | None = None)[source]#

Validate that non-trivial modules call _dbg() for diagnostics.

_dbg is the structured debug logger from oc_diagnostics (from oc_diagnostics import _dbg). Files shorter than 20 lines are considered trivial scaffolding and are exempt from this check.

Uses AST parsing to detect actual _dbg() call nodes, avoiding false positives from comments or string literals.

oct.linter.oct_lint.check_dependencies_section(text: str)[source]#

Validate the Dependencies section of a module docstring at strict profile.

Spec reference: Appendix C — Dependencies docstring format. Each entry must follow - module_name (purpose description) so oct deps can parse the dependency graph.

Returns (ok, msg). Empty Dependencies sections are accepted (a module may have no project-internal dependencies); malformed entries are rejected.

oct.linter.oct_lint.check_diagnostics_profile(text: str)[source]#

Validate that a Diagnostics profile is present in the module docstring.

Scopes the search to the module docstring (text before the first def/class) to avoid false positives from in-code string literals that happen to contain ‘L2’, ‘L3’, ‘L4’, ‘Diagnostics’, or ‘Domain:’. Falls back to the full file if no module docstring is found.

oct.linter.oct_lint.check_docstring(text: str)[source]#

Validate that a module docstring exists and contains the required Option C sections.

oct.linter.oct_lint.check_func_pattern(text: str, tree: Module | None = None)[source]#

Validate that every function body containing _dbg() first defines func = "function_name".

Uses ast.parse() to build a syntax tree, then walks each function’s body (stopping at nested function boundaries) to check for the pattern. Exempt trivial files (< 20 lines) — same threshold as check_dbg_usage.

Delegates to find_func_pattern_violations() for the actual detection; this wrapper converts the result into a (bool, message) pair for the linter pipeline.

oct.linter.oct_lint.check_syntax_warnings(text: str, warnings_list: list[str] | None = None)[source]#

Check for Python syntax warnings (invalid escape sequences etc.).

Python 3.12+ emits SyntaxWarning for invalid escape sequences in string literals. These will become SyntaxError in a future Python version, so they should be surfaced as lint findings.

oct.linter.oct_lint.check_tests_exist(module_path: Path, ctx: LinterContext)[source]#

Validate that at least one test file references the module either by naming convention (test_<module>.py) or by an explicit import statement (import <module> or from <module>).

Two-stage check, now O(1) per call: consults the index built once per run_linter() invocation by build_test_index().

oct.linter.oct_lint.check_type_hints(text: str, tree: Module | None = None, compact_mode: bool = False)[source]#

Validate that public functions have type annotations on all parameters and a return annotation.

Private functions (leading underscore) and dunder methods are always exempt — they are internal implementation details (OI-417). The compact_mode parameter is retained for API compatibility but no longer changes behaviour.

Trivial files (< 20 lines) are exempt.

oct.linter.oct_lint.expected_path_header(path: Path, ctx: LinterContext) str[source]#

Compute the expected file identity header for a given module path.

oct.linter.oct_lint.find_func_pattern_violations(text: str, tree: Module | None = None) list[tuple[str, int]][source]#

Return [(func_name, lineno), ...] for every function that calls _dbg() without a prior func = "..." assignment.

Returns an empty list for trivial files (< 20 lines) or files with syntax errors. This is the single source of truth for func-pattern detection — used by check_func_pattern() (linter pass/fail) and by oct.formatter.oct_format.fix_func_pattern() (auto-fix).

oct.linter.oct_lint.find_python_files(root: Path, exclude_dirs: set | None = None, wildcard_exclude_dirs: list | None = None)[source]#

Yield all Python files under root, excluding the linter itself and excluded directories.

Parameters:
  • root – The project root to scan.

  • exclude_dirs – Set of directory names to exclude. Defaults to the module-level EXCLUDE_DIRS constant.

  • wildcard_exclude_dirs – List of substrings; directories whose name contains any of these are excluded. Defaults to WILDCARD_EXCLUDE_DIRS.

oct.linter.oct_lint.fix_header_block(path: Path, text: str, ctx: LinterContext) bool[source]#

Rewrite the top of the file to enforce the Option C 4-line header. The original file is archived in a .linter_archive directory.

oct.linter.oct_lint.is_excluded_dir(path: Path, exclude_dirs: set, wildcard_exclude_dirs: list) bool[source]#

Return True if the directory name should be excluded from scanning.

oct.linter.oct_lint.log(message: str, ctx: LinterContext | None = None) None[source]#

Write a single line to the linter log file.

OI-513: ctx is required in practice — every internal callsite threads it through. The None branch is kept as a silent no-op so external callers that import log() standalone do not crash.

oct.linter.oct_lint.main() None[source]#

Standalone entry point when running this module directly.

oct.linter.oct_lint.preview_header_fix(path: Path, ctx: LinterContext) str[source]#

Compute what fix_header_block would produce, without writing. Returns the corrected 4-line header block as a string.

oct.linter.oct_lint.print_header(title: str, ctx: LinterContext | None = None) None[source]#

Write a section header to the log.

oct.linter.oct_lint.read_file(path: Path, ctx: LinterContext | None = None) str[source]#

Read a file as UTF-8, returning an empty string on failure.

oct.linter.oct_lint.report(result: bool, message: str, fix_mode: bool = False, ctx: LinterContext | None = None) bool[source]#

Log a PASS/FAIL (or FIXED) line and return the result.

oct.linter.oct_lint.run_linter(project_root: Path, argv: list[str] | None = None) int[source]#

Run the Option C linter on the given project root.

Parameters:
  • project_root – The detected Option C project root directory.

  • argv – Optional list of command-line arguments (e.g. [”–fix-headers”]). If None, argparse will use sys.argv.

Returns:

Exit code: 0 if all checks passed, 1 if any failures detected.

Return type:

int

oct.linter.oct_lint.validate_header_block(lines: list[str], path: Path, ctx: LinterContext)[source]#

Validate the mandatory 4-line header block.

Returns (ok, errors) where: - ok = True if header block is correct - errors = list of error messages