1- use camino:: Utf8PathBuf ;
2- use rustc_hash:: FxHashSet ;
1+ use camino:: { Utf8Path , Utf8PathBuf } ;
2+ use rustc_hash:: { FxHashMap , FxHashSet } ;
33use std:: hash:: Hash ;
44use std:: { fmt:: Debug , sync:: Arc } ;
55
@@ -23,6 +23,14 @@ pub trait AnalyzerPlugin: Debug + Send + Sync {
2323 fn query ( & self ) -> Vec < RawSyntaxKind > ;
2424
2525 fn evaluate ( & self , node : AnySyntaxNode , path : Arc < Utf8PathBuf > ) -> Vec < RuleDiagnostic > ;
26+
27+ /// Returns true if this plugin should run on the given file path.
28+ ///
29+ /// Stub that always returns `true` — file-scoping will be implemented
30+ /// in a companion PR (#9171) via the `includes` plugin option.
31+ fn applies_to_file ( & self , _path : & Utf8Path ) -> bool {
32+ true
33+ }
2634}
2735
2836#[ derive( Copy , Clone , Debug , Eq , PartialEq , Hash ) ]
@@ -37,6 +45,10 @@ pub enum PluginTargetLanguage {
3745pub struct PluginVisitor < L : Language > {
3846 query : FxHashSet < L :: Kind > ,
3947 plugin : Arc < Box < dyn AnalyzerPlugin > > ,
48+
49+ /// When set, all nodes in this subtree are skipped until we leave it.
50+ /// Used to skip subtrees that fall entirely outside the analysis range
51+ /// (see the `ctx.range` check in `visit`).
4052 skip_subtree : Option < SyntaxNode < L > > ,
4153}
4254
@@ -102,6 +114,10 @@ where
102114 return ;
103115 }
104116
117+ if !self . plugin . applies_to_file ( & ctx. options . file_path ) {
118+ return ;
119+ }
120+
105121 let rule_timer = profiling:: start_plugin_rule ( "plugin" ) ;
106122 let diagnostics = self
107123 . plugin
@@ -126,3 +142,139 @@ where
126142 ctx. signal_queue . extend ( signals) ;
127143 }
128144}
145+
146+ /// A batched syntax visitor that evaluates multiple plugins in a single visitor.
147+ ///
148+ /// Instead of registering N separate `PluginVisitor` instances (one per plugin),
149+ /// this holds all plugins together and dispatches using a kind-to-plugin lookup
150+ /// map. This reduces visitor-dispatch overhead and enables O(1) kind matching
151+ /// per node instead of iterating all plugins.
152+ pub struct BatchPluginVisitor < L : Language > {
153+ plugins : Vec < Arc < Box < dyn AnalyzerPlugin > > > ,
154+
155+ /// Maps each syntax kind to the indices of plugins that query for it.
156+ kind_to_plugins : FxHashMap < L :: Kind , Vec < usize > > ,
157+
158+ /// When set, all nodes in this subtree are skipped until we leave it.
159+ /// Used to skip subtrees that fall entirely outside the analysis range
160+ /// (see the `ctx.range` check in `visit`).
161+ skip_subtree : Option < SyntaxNode < L > > ,
162+
163+ /// Cached per-plugin results of `applies_to_file`. Populated lazily on
164+ /// first `WalkEvent::Enter` — the file path is constant for the entire walk.
165+ applicable : Option < Vec < bool > > ,
166+ }
167+
168+ impl < L > BatchPluginVisitor < L >
169+ where
170+ L : Language + ' static ,
171+ L :: Kind : Eq + Hash ,
172+ {
173+ /// Creates a batched plugin visitor from a slice of plugins.
174+ ///
175+ /// # Safety
176+ /// Caller must ensure all plugins target language `L`. The `RawSyntaxKind`
177+ /// values returned by each plugin's `query()` are converted to `L::Kind`
178+ /// via `from_raw` without validation.
179+ pub unsafe fn new_unchecked ( plugins : AnalyzerPluginSlice ) -> Self {
180+ let mut all_plugins = Vec :: with_capacity ( plugins. len ( ) ) ;
181+ let mut kind_to_plugins: FxHashMap < L :: Kind , Vec < usize > > = FxHashMap :: default ( ) ;
182+
183+ for ( idx, plugin) in plugins. iter ( ) . enumerate ( ) {
184+ all_plugins. push ( Arc :: clone ( plugin) ) ;
185+ let mut seen_kinds = FxHashSet :: default ( ) ;
186+ for raw_kind in plugin. query ( ) {
187+ let kind = L :: Kind :: from_raw ( raw_kind) ;
188+ if seen_kinds. insert ( kind) {
189+ kind_to_plugins. entry ( kind) . or_default ( ) . push ( idx) ;
190+ }
191+ }
192+ }
193+
194+ Self {
195+ plugins : all_plugins,
196+ kind_to_plugins,
197+ skip_subtree : None ,
198+ applicable : None ,
199+ }
200+ }
201+ }
202+
203+ impl < L > Visitor for BatchPluginVisitor < L >
204+ where
205+ L : Language + ' static ,
206+ L :: Kind : Eq + Hash ,
207+ {
208+ type Language = L ;
209+
210+ fn visit (
211+ & mut self ,
212+ event : & WalkEvent < SyntaxNode < Self :: Language > > ,
213+ ctx : VisitorContext < Self :: Language > ,
214+ ) {
215+ let node = match event {
216+ WalkEvent :: Enter ( node) => node,
217+ WalkEvent :: Leave ( node) => {
218+ if let Some ( skip_subtree) = & self . skip_subtree
219+ && skip_subtree == node
220+ {
221+ self . skip_subtree = None ;
222+ }
223+
224+ return ;
225+ }
226+ } ;
227+
228+ if self . skip_subtree . is_some ( ) {
229+ return ;
230+ }
231+
232+ if let Some ( range) = ctx. range
233+ && node. text_range_with_trivia ( ) . ordering ( range) . is_ne ( )
234+ {
235+ self . skip_subtree = Some ( node. clone ( ) ) ;
236+ return ;
237+ }
238+
239+ let kind = node. kind ( ) ;
240+
241+ let Some ( plugin_indices) = self . kind_to_plugins . get ( & kind) else {
242+ return ;
243+ } ;
244+
245+ let applicable = self . applicable . get_or_insert_with ( || {
246+ self . plugins
247+ . iter ( )
248+ . map ( |p| p. applies_to_file ( & ctx. options . file_path ) )
249+ . collect ( )
250+ } ) ;
251+
252+ for & idx in plugin_indices {
253+ if !applicable[ idx] {
254+ continue ;
255+ }
256+
257+ let plugin = & self . plugins [ idx] ;
258+ let rule_timer = profiling:: start_plugin_rule ( "plugin" ) ;
259+ let diagnostics = plugin. evaluate ( node. clone ( ) . into ( ) , ctx. options . file_path . clone ( ) ) ;
260+ rule_timer. stop ( ) ;
261+
262+ let signals = diagnostics. into_iter ( ) . map ( |diagnostic| {
263+ let name = diagnostic
264+ . subcategory
265+ . clone ( )
266+ . unwrap_or_else ( || "anonymous" . into ( ) ) ;
267+
268+ SignalEntry {
269+ text_range : diagnostic. span ( ) . unwrap_or_default ( ) ,
270+ signal : Box :: new ( PluginSignal :: < L > :: new ( diagnostic) ) ,
271+ rule : SignalRuleKey :: Plugin ( name. into ( ) ) ,
272+ category : RuleCategory :: Lint ,
273+ instances : Default :: default ( ) ,
274+ }
275+ } ) ;
276+
277+ ctx. signal_queue . extend ( signals) ;
278+ }
279+ }
280+ }
0 commit comments