Coverage for lintro / plugins / protocol.py: 94%

31 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-04-03 18:53 +0000

1"""Plugin protocol defining the contract for all Lintro tools. 

2 

3This module defines the core abstractions for Lintro's plugin system: 

4- ToolDefinition: Metadata describing what a tool IS 

5- LintroPlugin: Protocol contract that all tools must satisfy 

6 

7Example: 

8 >>> from lintro.plugins.protocol import ToolDefinition, LintroPlugin 

9 >>> from lintro.plugins.base import BaseToolPlugin 

10 >>> 

11 >>> class MyPlugin(BaseToolPlugin): 

12 ... @property 

13 ... def definition(self) -> ToolDefinition: 

14 ... return ToolDefinition(name="my-tool", description="My custom tool") 

15""" 

16 

17from __future__ import annotations 

18 

19from dataclasses import dataclass, field 

20from typing import TYPE_CHECKING, Protocol, runtime_checkable 

21 

22from lintro.enums.tool_type import ToolType 

23 

24if TYPE_CHECKING: 

25 from lintro.models.core.tool_result import ToolResult 

26 

27 

28@dataclass(frozen=True) 

29class ToolDefinition: 

30 """Metadata describing a Lintro tool. 

31 

32 This is the single source of truth for what a tool IS. 

33 Implementation details go in separate files (under implementations/). 

34 

35 Attributes: 

36 name: Unique identifier for the tool (lowercase, e.g., "hadolint"). 

37 description: Human-readable description of what the tool does. 

38 can_fix: Whether the tool can auto-fix issues. 

39 tool_type: Bitmask of ToolType flags describing capabilities. 

40 file_patterns: Glob patterns for files this tool operates on. 

41 priority: Execution priority (lower = runs first). Default is 50. 

42 conflicts_with: Names of tools that conflict with this one. 

43 native_configs: Config files the tool respects natively 

44 (Lintro won't interfere). 

45 version_command: Command to check tool version 

46 (e.g., ["hadolint", "--version"]). 

47 min_version: Minimum required version string. 

48 default_options: Default tool-specific options. 

49 default_timeout: Default execution timeout in seconds. 

50 """ 

51 

52 # Identity 

53 name: str 

54 description: str 

55 

56 # Capabilities 

57 can_fix: bool = False 

58 tool_type: ToolType = ToolType.LINTER 

59 

60 # File targeting 

61 file_patterns: list[str] = field(default_factory=list) 

62 

63 # Execution 

64 priority: int = 50 

65 conflicts_with: list[str] = field(default_factory=list) 

66 

67 # Native config files this tool respects (Lintro should NOT interfere) 

68 native_configs: list[str] = field(default_factory=list) 

69 

70 # Version checking 

71 version_command: list[str] | None = None 

72 min_version: str | None = None 

73 

74 # Default options 

75 default_options: dict[str, object] = field(default_factory=dict) 

76 default_timeout: int = 30 

77 

78 def __post_init__(self) -> None: 

79 """Validate tool definition. 

80 

81 Raises: 

82 ValueError: If name is empty or priority is negative. 

83 """ 

84 if not self.name: 

85 raise ValueError("Tool name cannot be empty") 

86 if self.priority < 0: 

87 raise ValueError(f"Tool priority must be non-negative, got {self.priority}") 

88 

89 

90@runtime_checkable 

91class LintroPlugin(Protocol): 

92 """Contract that all Lintro tools must satisfy. 

93 

94 This protocol defines the interface for tool plugins. Tools can be 

95 implemented by inheriting from BaseToolPlugin or by implementing 

96 this protocol directly. 

97 

98 Example: 

99 >>> class MyPlugin: 

100 ... @property 

101 ... def definition(self) -> ToolDefinition: 

102 ... return ToolDefinition(name="my-tool", description="My tool") 

103 ... 

104 ... def check(self, paths, options) -> ToolResult: 

105 ... ... # Implementation here 

106 ... 

107 ... def fix(self, paths, options) -> ToolResult: 

108 ... raise NotImplementedError("No fixing") 

109 ... 

110 ... def set_options(self, **kwargs: object) -> None: 

111 ... # Set tool options 

112 ... ... 

113 """ 

114 

115 @property 

116 def definition(self) -> ToolDefinition: 

117 """Return the tool's metadata.""" 

118 ... 

119 

120 def check(self, paths: list[str], options: dict[str, object]) -> ToolResult: 

121 """Check files for issues. 

122 

123 Args: 

124 paths: List of file or directory paths to check. 

125 options: Tool-specific options that override defaults. 

126 

127 Returns: 

128 ToolResult containing check results and any issues found. 

129 """ 

130 ... 

131 

132 def fix(self, paths: list[str], options: dict[str, object]) -> ToolResult: 

133 """Fix issues in files. 

134 

135 Args: 

136 paths: List of file or directory paths to fix. 

137 options: Tool-specific options that override defaults. 

138 

139 Returns: 

140 ToolResult containing fix results and any remaining issues. 

141 """ 

142 ... 

143 

144 def set_options(self, **kwargs: object) -> None: 

145 """Set tool-specific options. 

146 

147 Args: 

148 **kwargs: Tool-specific options to set. 

149 """ 

150 ... 

151 

152 def doc_url(self, code: str) -> str | None: 

153 """Return a documentation URL for the given rule code. 

154 

155 Args: 

156 code: The rule/error code (e.g., "E501", "SC2086"). 

157 

158 Returns: 

159 Documentation URL string, or None if no docs available. 

160 """ 

161 ...