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
« 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 -*-
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#============================================================================#
21''' Rule registry system for discovery and instantiation. '''
24from . import __
25from .base import BaseRule as _BaseRule
28class RuleDescriptor( __.immut.DataclassObject ):
29 ''' Describes rule metadata for registry and configuration. '''
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.' ) ]
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]
67class RuleRegistryManager:
68 ''' Manages bidirectional mapping between VBL codes and rules. '''
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 } )
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 )
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 )
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 )
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 )
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 )