"""
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