Source code for tb_lint.linters.verible_linter

"""
Verible Linter Implementation

Company: Copyright (c) 2025  BTA Design Services  
         Licensed under the MIT License.

Description: Adapter for Verible style and syntax linting
"""

import os
import sys
import shutil
import subprocess
import re
from typing import List, Optional
from dataclasses import dataclass

# Add parent directory to path for imports
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from core.base_linter import BaseLinter, LinterResult
from core.base_rule import RuleViolation, RuleSeverity
from core.linter_registry import register_linter


[docs] @register_linter class VeribleLinter(BaseLinter): """ Verible syntax and style linter This linter wraps the verible-verilog-lint tool and converts its output into the unified format. """ @property def name(self) -> str: return "verible" @property def supported_extensions(self) -> List[str]: return ['.sv', '.svh', '.v', '.vh']
[docs] def __init__(self, config: Optional[dict] = None): """ Initialize Verible linter Args: config: Configuration dictionary """ # Initialize base class first super().__init__(config) # Find Verible binary self.verible_bin = self._find_verible_binary() if not self.verible_bin: print("ERROR: verible-verilog-lint not found", file=sys.stderr) sys.exit(1) # Get config file path self.config_file = self._find_config_file()
def _find_verible_binary(self) -> Optional[str]: """Find verible-verilog-lint binary using environment variables""" # First try PATH verible_bin = shutil.which("verible-verilog-lint") if verible_bin: return verible_bin # Try VERIBLE_HOME environment variable verible_home = os.environ.get('VERIBLE_HOME') if verible_home: verible_bin = os.path.join(verible_home, 'bin', 'verible-verilog-lint') if os.path.exists(verible_bin): return verible_bin return None def _find_config_file(self) -> Optional[str]: """Find Verible rules config file""" # Check if config specifies a rules file if 'rules_file' in self.config: rules_file = self.config['rules_file'] if os.path.exists(rules_file): return rules_file # Look for default config in script directory script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) default_config = os.path.join(script_dir, '.rules.verible_lint') if os.path.exists(default_config): return default_config return None def _register_rules(self): """ Register Verible rules Note: Verible is an external tool, so rules are not registered individually in our framework. This method is required by the base class but doesn't do anything for Verible. """ pass
[docs] def prepare_context(self, file_path: str, file_content: str) -> Optional[any]: """ Prepare context for Verible For Verible, we don't need to prepare any context since it's an external tool. We just return a dummy context. Returns: Empty dict (Verible doesn't need context preparation) """ return {}
[docs] def lint_file(self, file_path: str) -> LinterResult: """ Lint a single file using Verible Override the base method to call verible-verilog-lint directly Args: file_path: Path to file to lint Returns: LinterResult containing violations found """ result = LinterResult(linter_name=self.name) if not os.path.exists(file_path): result.add_error(file_path, "File not found") return result # Build command cmd = [self.verible_bin] # Add config file if available if self.config_file: cmd.append(f"--rules_config={self.config_file}") # Add file cmd.append(file_path) try: # Run Verible proc = subprocess.run( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True ) # Parse output violations = self._parse_verible_output(proc.stdout, file_path) for violation in violations: result.add_violation(violation) result.files_checked = 1 except Exception as e: result.add_error(file_path, f"Failed to run Verible: {str(e)}") return result
def _parse_verible_output(self, output: str, file_path: str) -> List[RuleViolation]: """ Parse Verible output into RuleViolation objects Verible output format: path/to/file.sv:line:col-range: message [Style: category] [rule-name] Args: output: Verible stdout file_path: Path to file being checked Returns: List of RuleViolation objects """ violations = [] # Get configuration - matches NaturalDocs structure linter_rules = self.config.get('linter_rules', {}) severity_levels = self.config.get('severity_levels', {}) for line in output.split('\n'): if not line.strip(): continue # Match Verible's output format match = re.match( r'^([^:]+):(\d+):(\d+(?:-\d+)?):\s*(.+?)\s*\[([^\]]+)\]\s*\[([^\]]+)\]', line ) if match: file_name = match.group(1) line_num = int(match.group(2)) col_str = match.group(3) # Could be "32" or "32-33" message = match.group(4).strip() category = match.group(5).strip() # e.g., "Style: trailing-spaces" rule_name = match.group(6).strip() # e.g., "no-trailing-spaces" # Create rule ID rule_id = f"[VB_{rule_name.upper().replace('-', '_')}]" # Check if rule is enabled (like NaturalDocs linter_rules) if rule_id in linter_rules and not linter_rules[rule_id]: # Rule is disabled, skip it continue # Extract column start column = int(col_str.split('-')[0]) # Get severity from severity_levels (like NaturalDocs) severity_str = severity_levels.get(rule_id, 'WARNING') severity = RuleSeverity[severity_str] violation = RuleViolation( file=file_name, line=line_num, column=column, severity=severity, message=message, rule_id=rule_id ) violations.append(violation) return violations