Coverage for sources / vibelinter / rules / registry.py: 63%

35 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-12-01 02:35 +0000

1# vim: set filetype=python fileencoding=utf-8: 

2# -*- coding: utf-8 -*- 

3 

4#============================================================================# 

5# # 

6# Licensed under the Apache License, Version 2.0 (the "License"); # 

7# you may not use this file except in compliance with the License. # 

8# You may obtain a copy of the License at # 

9# # 

10# http://www.apache.org/licenses/LICENSE-2.0 # 

11# # 

12# Unless required by applicable law or agreed to in writing, software # 

13# distributed under the License is distributed on an "AS IS" BASIS, # 

14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

15# See the License for the specific language governing permissions and # 

16# limitations under the License. # 

17# # 

18#============================================================================# 

19 

20 

21''' Rule registry system for discovery and instantiation. ''' 

22 

23 

24from . import __ 

25from .base import BaseRule as _BaseRule 

26 

27 

28class RuleDescriptor( __.immut.DataclassObject ): 

29 ''' Describes rule metadata for registry and configuration. ''' 

30 

31 vbl_code: __.typx.Annotated[ 

32 str, 

33 __.ddoc.Doc( 'VBL code identifier (e.g., "VBL101").' ) ] 

34 descriptive_name: __.typx.Annotated[ 

35 str, 

36 __.ddoc.Doc( 

37 'Hyphen-separated descriptive name (e.g., ' 

38 '"blank-line-elimination").' 

39 ) ] 

40 description: __.typx.Annotated[ 

41 str, 

42 __.ddoc.Doc( 'Human-readable rule description.' ) ] 

43 category: __.typx.Annotated[ 

44 str, 

45 __.ddoc.Doc( 

46 'Rule category (readability, discoverability, robustness).' 

47 ) ] 

48 subcategory: __.typx.Annotated[ 

49 str, 

50 __.ddoc.Doc( 

51 'Rule subcategory (compactness, nomenclature, navigation).' 

52 ) ] 

53 rule_class: __.typx.Annotated[ 

54 type, 

55 __.ddoc.Doc( 'Rule class for instantiation.' ) ] 

56 

57 

58# Type aliases for registry 

59RuleRegistry: __.typx.TypeAlias = ( 

60 __.immut.Dictionary[ str, RuleDescriptor ] ) 

61RuleClassFactory: __.typx.TypeAlias = __.cabc.Callable[ 

62 [ str, __.libcst.metadata.MetadataWrapper, tuple[ str, ... ] ], 

63 _BaseRule 

64] 

65 

66 

67class RuleRegistryManager: 

68 ''' Manages bidirectional mapping between VBL codes and rules. ''' 

69 

70 def __init__( 

71 self, 

72 registry: __.typx.Annotated[ 

73 __.cabc.Mapping[ str, RuleDescriptor ], 

74 __.ddoc.Doc( 'Mapping of VBL codes to rule descriptors.' ) ] 

75 ) -> None: 

76 self.registry = __.immut.Dictionary( registry ) 

77 # Build reverse mapping from descriptive names to VBL codes 

78 self._name_to_code = __.immut.Dictionary( { 

79 descriptor.descriptive_name: vbl_code 

80 for vbl_code, descriptor in registry.items( ) 

81 } ) 

82 

83 def resolve_rule_identifier( 

84 self, 

85 identifier: __.typx.Annotated[ 

86 str, 

87 __.ddoc.Doc( 'VBL code or descriptive name to resolve.' ) ] 

88 ) -> __.typx.Annotated[ 

89 str, 

90 __.ddoc.Doc( 'Canonical VBL code for identifier.' ) ]: 

91 ''' Resolves VBL code or descriptive name to VBL code. ''' 

92 from ..exceptions import RuleRegistryInvalidity 

93 # Try as VBL code first 

94 if identifier in self.registry: 

95 return identifier 

96 # Try as descriptive name 

97 if identifier in self._name_to_code: 

98 return self._name_to_code[ identifier ] 

99 raise RuleRegistryInvalidity( identifier ) 

100 

101 def produce_rule_instance( 

102 self, 

103 vbl_code: __.typx.Annotated[ 

104 str, 

105 __.ddoc.Doc( 'VBL code for rule.' ) ], 

106 filename: __.typx.Annotated[ 

107 str, 

108 __.ddoc.Doc( 'Path to source file being analyzed.' ) ], 

109 wrapper: __.typx.Annotated[ 

110 __.libcst.metadata.MetadataWrapper, 

111 __.ddoc.Doc( 'LibCST metadata wrapper.' ) ], 

112 source_lines: __.typx.Annotated[ 

113 tuple[ str, ... ], 

114 __.ddoc.Doc( 'Source file lines.' ) ], 

115 **parameters: __.typx.Any 

116 ) -> __.typx.Annotated[ 

117 _BaseRule, 

118 __.ddoc.Doc( 'Instantiated rule ready for analysis.' ) ]: 

119 ''' Creates a rule instance from its VBL code. ''' 

120 from ..exceptions import RuleRegistryInvalidity 

121 if vbl_code not in self.registry: 121 ↛ 122line 121 didn't jump to line 122 because the condition on line 121 was never true

122 raise RuleRegistryInvalidity( vbl_code ) 

123 descriptor = self.registry[ vbl_code ] 

124 rule_class = descriptor.rule_class 

125 # Instantiate rule with parameters 

126 # Base parameters are filename, wrapper, source_lines 

127 # Additional parameters can be passed as keyword arguments 

128 return rule_class( 

129 filename = filename, 

130 wrapper = wrapper, 

131 source_lines = source_lines, 

132 **parameters 

133 ) 

134 

135 def survey_available_rules( self ) -> tuple[ RuleDescriptor, ... ]: 

136 ''' Returns all registered rule descriptors. ''' 

137 return tuple( 

138 descriptor 

139 for _, descriptor in sorted( self.registry.items( ) ) 

140 ) 

141 

142 def filter_rules_by_category( 

143 self, 

144 category: __.typx.Annotated[ 

145 str, 

146 __.ddoc.Doc( 'Category to filter by.' ) ] 

147 ) -> tuple[ RuleDescriptor, ... ]: 

148 ''' Returns rule descriptors matching category. ''' 

149 return tuple( 

150 descriptor 

151 for descriptor in self.survey_available_rules( ) 

152 if descriptor.category == category 

153 ) 

154 

155 def filter_rules_by_subcategory( 

156 self, 

157 subcategory: __.typx.Annotated[ 

158 str, 

159 __.ddoc.Doc( 'Subcategory to filter by.' ) ] 

160 ) -> tuple[ RuleDescriptor, ... ]: 

161 ''' Returns rule descriptors matching subcategory. ''' 

162 return tuple( 

163 descriptor 

164 for descriptor in self.survey_available_rules( ) 

165 if descriptor.subcategory == subcategory 

166 )