1mod assertions;
4mod ast;
5mod framework_callbacks;
6pub(crate) mod frameworks;
7mod handler;
8mod matcher;
9mod module_resolver;
10mod project_resolution;
11mod registrations;
12mod suppression;
13
14use self::assertions::AssertionBindings;
15use self::handler::{
16 bind_arguments, collect_handler_bindings, fingerprint_handler, fingerprint_method_handler,
17 resolve_handler, HandlerBinding,
18};
19#[cfg(test)]
20use self::handler::{bounded_source_snippet, MAX_HANDLER_SNIPPET_CHARS};
21pub use self::matcher::normalize_matcher;
22#[cfg(test)]
23use self::matcher::normalize_regular_expression;
24pub(crate) use self::matcher::rust_regex_expression;
25pub(crate) use self::matcher::semantic_regex_flags;
26use self::matcher::{matcher_value, normalize_matcher_with_flags};
27use self::matcher::{rust_regex_support, RegexSupport};
28use self::module_resolver::RegistrationResolver;
29use self::registrations::{
30 decorator_registration_name, detect_framework, detect_registrations, registration_callee,
31 registration_name, unresolved_registration_module, RegistrationCallee, RegistrationNames,
32};
33use self::suppression::inline_suppressions;
34use crate::model::{Framework, HandlerFingerprint, MatcherKind, SourceLocation, StepDefinition};
35use crate::source_adapter::{
36 adapter_for_language, grammar_for_language, AdapterSessionState, SourceAdapter,
37 SourceExtractionSession, StatefulSourceAdapter, INVALID_MATCHER_DIAGNOSTIC,
38 UNPARSEABLE_SOURCE_DIAGNOSTIC_PREFIX, UNRESOLVED_REGISTRATION_DIAGNOSTIC_PREFIX,
39 UNSUPPORTED_MATCHER_DIAGNOSTIC,
40};
41pub use crate::source_adapter::{
42 Extraction, ExtractionDiagnostic, ExtractionDiagnosticLevel, SourceFile, SourceLanguage,
43};
44use anyhow::{Context, Result};
45use std::collections::{BTreeMap, BTreeSet};
46use tree_sitter::{Node, Parser};
47
48pub(crate) struct TreeSitterSourceAdapter {
49 name: &'static str,
50 language: SourceLanguage,
51}
52
53pub(crate) static JAVASCRIPT_ADAPTER: TreeSitterSourceAdapter = TreeSitterSourceAdapter {
54 name: "javascript",
55 language: SourceLanguage::JavaScript,
56};
57pub(crate) static TYPESCRIPT_ADAPTER: TreeSitterSourceAdapter = TreeSitterSourceAdapter {
58 name: "typescript",
59 language: SourceLanguage::TypeScript,
60};
61pub(crate) static TSX_ADAPTER: TreeSitterSourceAdapter = TreeSitterSourceAdapter {
62 name: "tsx",
63 language: SourceLanguage::Tsx,
64};
65
66#[derive(Default)]
67pub(crate) struct TypeScriptExtractionSession {
68 resolver: RegistrationResolver,
69 configured_registrations: std::collections::BTreeSet<String>,
70 configured_assertion_modules: std::collections::BTreeSet<String>,
71}
72
73impl TypeScriptExtractionSession {
74 pub(crate) fn for_root(
75 root: &std::path::Path,
76 registrations: &[String],
77 assertion_modules: &[String],
78 ) -> Self {
79 Self {
80 resolver: RegistrationResolver::for_root(root),
81 configured_registrations: registrations.iter().cloned().collect(),
82 configured_assertion_modules: assertion_modules.iter().cloned().collect(),
83 }
84 }
85}
86
87impl AdapterSessionState for TypeScriptExtractionSession {
88 fn initialize(
89 root: Option<&std::path::Path>,
90 registrations: &[String],
91 assertion_modules: &[String],
92 ) -> Self {
93 match root {
94 Some(root) => Self::for_root(root, registrations, assertion_modules),
95 None => Self::default(),
96 }
97 }
98}
99
100impl StatefulSourceAdapter for TreeSitterSourceAdapter {
101 type State = TypeScriptExtractionSession;
102}
103
104impl SourceAdapter for TreeSitterSourceAdapter {
105 fn name(&self) -> &'static str {
106 self.name
107 }
108
109 fn language(&self) -> SourceLanguage {
110 self.language
111 }
112
113 fn extract(&self, source: &str, file: &SourceFile) -> Result<Extraction> {
114 let mut session = SourceExtractionSession::default();
115 self.extract_with_session(source, file, &mut session)
116 }
117
118 fn extract_with_session(
119 &self,
120 source: &str,
121 file: &SourceFile,
122 session: &mut SourceExtractionSession,
123 ) -> Result<Extraction> {
124 if file.language != self.language {
125 anyhow::bail!(
126 "{} adapter cannot parse {:?} source {}",
127 self.name,
128 file.language,
129 file.path.display()
130 );
131 }
132 extract_detailed_impl(
133 source,
134 file,
135 session.state::<TypeScriptExtractionSession>()?,
136 )
137 }
138}
139
140struct AdapterContext<'source, 'tree> {
141 source: &'source [u8],
142 source_lines: &'source [&'source str],
143 file: &'source SourceFile,
144 framework: Framework,
145 registrations: &'source RegistrationNames,
146 framework_calls: &'source framework_callbacks::FrameworkCalls,
147 assertions: &'source AssertionBindings,
148 handler_bindings: &'source BTreeMap<String, Vec<HandlerBinding<'tree>>>,
149}
150
151#[derive(Default)]
152struct UnresolvedRegistrationCalls {
153 count: usize,
154 first_location: Option<SourceLocation>,
155 modules: BTreeMap<String, (usize, SourceLocation)>,
156 passed: usize,
158 first_passed: Option<SourceLocation>,
159}
160
161impl UnresolvedRegistrationCalls {
162 fn record(&mut self, location: SourceLocation) {
163 self.count = self.count.saturating_add(1);
164 self.first_location.get_or_insert(location);
165 }
166
167 fn record_passed(&mut self, location: SourceLocation) {
168 self.passed = self.passed.saturating_add(1);
169 self.first_passed.get_or_insert(location);
170 }
171
172 fn record_module(&mut self, module: &str, location: SourceLocation) {
173 let entry = self
174 .modules
175 .entry(module.to_owned())
176 .or_insert_with(|| (0, location));
177 entry.0 = entry.0.saturating_add(1);
178 }
179}
180
181pub fn extract_file(file: &SourceFile) -> Result<Vec<StepDefinition>> {
183 Ok(extract_file_detailed(file)?.definitions)
184}
185
186pub fn extract_file_detailed(file: &SourceFile) -> Result<Extraction> {
188 adapter_for_language(file.language).extract_file(file)
189}
190
191pub fn extract(source: &str, file: &SourceFile) -> Result<Vec<StepDefinition>> {
193 Ok(extract_detailed(source, file)?.definitions)
194}
195
196pub fn extract_detailed(source: &str, file: &SourceFile) -> Result<Extraction> {
198 adapter_for_language(file.language).extract(source, file)
199}
200
201fn extract_detailed_impl(
202 source: &str,
203 file: &SourceFile,
204 session: &mut TypeScriptExtractionSession,
205) -> Result<Extraction> {
206 let mut parser = Parser::new();
207 parser
208 .set_language(&grammar_for_language(file.language))
209 .context("failed to initialize the JavaScript/TypeScript parser")?;
210 let tree = parser
211 .parse(source, None)
212 .context("Tree-sitter did not return a syntax tree")?;
213
214 let root = tree.root_node();
215 let source_bytes = source.as_bytes();
216 let source_lines: Vec<_> = source.lines().collect();
217 let framework = detect_framework(root, source_bytes);
218 let registrations = detect_registrations(
219 root,
220 source_bytes,
221 framework,
222 &file.path,
223 &mut session.resolver,
224 &session.configured_registrations,
225 )?;
226 let framework = registrations.framework;
227 let framework_calls = framework_callbacks::discover(
228 root,
229 source_bytes,
230 &file.path,
231 &mut session.resolver,
232 ®istrations,
233 )?;
234 let assertions = AssertionBindings::discover(
235 root,
236 source_bytes,
237 ®istrations,
238 &session.configured_assertion_modules,
239 );
240 let mut handler_bindings = BTreeMap::new();
241 collect_handler_bindings(root, source_bytes, &mut handler_bindings);
242 let context = AdapterContext {
243 source: source_bytes,
244 source_lines: &source_lines,
245 file,
246 framework,
247 registrations: ®istrations,
248 framework_calls: &framework_calls,
249 assertions: &assertions,
250 handler_bindings: &handler_bindings,
251 };
252 let mut definitions = Vec::new();
253 let mut diagnostics = Vec::new();
254 let mut unresolved_registration_calls = UnresolvedRegistrationCalls::default();
255 if root.has_error() {
256 let syntax_node = first_syntax_error(root).unwrap_or(root);
257 diagnostics.push(ExtractionDiagnostic {
262 level: ExtractionDiagnosticLevel::Warning,
263 location: node_location(file, syntax_node, source_bytes),
264 message: format!("{UNPARSEABLE_SOURCE_DIAGNOSTIC_PREFIX}, so analysis is incomplete; fix the syntax, use a .tsx extension for JSX, or narrow definition discovery"),
265 });
266 }
267 collect_calls(
268 root,
269 &context,
270 &mut definitions,
271 &mut diagnostics,
272 &mut unresolved_registration_calls,
273 );
274 if let Some(location) = unresolved_registration_calls.first_location {
275 diagnostics.push(ExtractionDiagnostic {
276 level: ExtractionDiagnosticLevel::Warning,
277 location,
278 message: format!(
279 "{UNRESOLVED_REGISTRATION_DIAGNOSTIC_PREFIX} {} call(s) look like step registrations but could not be resolved to a supported registration import or global; definitions may be missing",
280 unresolved_registration_calls.count
281 ),
282 });
283 }
284 if let Some(location) = unresolved_registration_calls.first_passed {
285 diagnostics.push(ExtractionDiagnostic {
286 level: ExtractionDiagnosticLevel::Warning,
287 location,
288 message: format!(
289 "{UNRESOLVED_REGISTRATION_DIAGNOSTIC_PREFIX} {} call(s) pass a step registration to a function the analyzer does not model; definitions may be missing",
290 unresolved_registration_calls.passed
291 ),
292 });
293 }
294 let mut explained_modules = BTreeSet::new();
295 for (module, (count, location)) in unresolved_registration_calls.modules {
296 let cause = registrations::unresolved_module_reason(®istrations, &module)
297 .map(|reason| format!(" ({reason})"))
298 .unwrap_or_default();
299 diagnostics.push(ExtractionDiagnostic {
300 level: ExtractionDiagnosticLevel::Warning,
301 location,
302 message: format!(
303 "{UNRESOLVED_REGISTRATION_DIAGNOSTIC_PREFIX} {count} call(s) import a known registration through unresolved module `{module}`{cause}; definitions may be missing"
304 ),
305 });
306 explained_modules.insert(module);
307 }
308 for (module, recorded) in registrations::unresolved_module_reasons(®istrations) {
313 if explained_modules.contains(module) {
314 continue;
315 }
316 let line = recorded.row + 1;
317 let column = unicode_column(source_bytes, recorded.byte_offset, recorded.byte_column);
318 diagnostics.push(ExtractionDiagnostic {
319 level: ExtractionDiagnosticLevel::Warning,
320 location: SourceLocation::new(&file.path, line, column, line, column),
321 message: format!(
322 "{UNRESOLVED_REGISTRATION_DIAGNOSTIC_PREFIX} module `{module}` could not be resolved statically ({}); definitions may be missing",
323 recorded.reason
324 ),
325 });
326 }
327 definitions.sort_by(|left, right| {
328 left.location
329 .line
330 .cmp(&right.location.line)
331 .then(left.location.column.cmp(&right.location.column))
332 });
333 Ok(Extraction {
334 definitions,
335 diagnostics,
336 })
337}
338
339fn collect_calls<'tree>(
340 node: Node<'tree>,
341 context: &AdapterContext<'_, 'tree>,
342 definitions: &mut Vec<StepDefinition>,
343 diagnostics: &mut Vec<ExtractionDiagnostic>,
344 unresolved_registration_calls: &mut UnresolvedRegistrationCalls,
345) {
346 let mut stack = vec![node];
347 while let Some(node) = stack.pop() {
348 if node.kind() == "call_expression" {
349 if let Some(unresolved) = unresolved_registration_call(node, context) {
350 let location = node_location(context.file, node, context.source);
351 match unresolved {
352 UnresolvedRegistration::Generic => {
353 unresolved_registration_calls.record(location)
354 }
355 UnresolvedRegistration::Module(module) => {
356 unresolved_registration_calls.record_module(module, location)
357 }
358 UnresolvedRegistration::Passed => {
359 unresolved_registration_calls.record_passed(location)
360 }
361 }
362 }
363 let definition = if node
364 .parent()
365 .is_some_and(|parent| parent.kind() == "decorator")
366 {
367 extract_decorator(node, context, diagnostics)
368 } else {
369 extract_call(node, context, diagnostics)
370 };
371 if let Some(definition) = definition {
372 definitions.push(definition);
373 }
374 }
375 let mut cursor = node.walk();
376 let children: Vec<_> = node.named_children(&mut cursor).collect();
377 stack.extend(children.into_iter().rev());
378 }
379}
380
381enum UnresolvedRegistration<'a> {
382 Generic,
383 Module(&'a str),
384 Passed,
386}
387
388fn arguments_pass_registration(arguments: Node<'_>, context: &AdapterContext<'_, '_>) -> bool {
395 let mut stack = vec![arguments];
396 while let Some(node) = stack.pop() {
397 let passed = match node.kind() {
398 "call_expression" => {
401 let bound = node
402 .child_by_field_name("function")
403 .filter(|function| function.kind() == "member_expression")
404 .filter(|function| {
405 function
406 .child_by_field_name("property")
407 .is_some_and(|property| node_text(property, context.source) == "bind")
408 })
409 .and_then(|function| function.child_by_field_name("object"));
410 stack.extend(bound);
411 continue;
412 }
413 "identifier" | "member_expression" | "subscript_expression" => {
414 registration_name(node, context.source, context.registrations).is_some()
415 || decorator_registration_name(node, context.source, context.registrations)
416 .is_some()
417 }
418 "shorthand_property_identifier" => context
420 .registrations
421 .recognizes_alias_at(node, node_text(node, context.source)),
422 _ => false,
423 };
424 if passed {
425 return true;
426 }
427 push_value_positions(node, context.source, &mut stack);
428 }
429 false
430}
431
432fn push_value_positions<'tree>(node: Node<'tree>, source: &[u8], stack: &mut Vec<Node<'tree>>) {
438 let field = |name: &str| node.child_by_field_name(name);
439 match node.kind() {
440 "arguments"
441 | "array"
442 | "object"
443 | "parenthesized_expression"
444 | "as_expression"
445 | "satisfies_expression"
446 | "non_null_expression"
447 | "await_expression" => {
448 let mut cursor = node.walk();
449 stack.extend(node.named_children(&mut cursor));
450 }
451 "pair" | "assignment_expression" => stack.extend(field("value").or(field("right"))),
452 "spread_element" => stack.extend(
455 node.named_child(0)
456 .filter(|operand| matches!(operand.kind(), "array" | "object")),
457 ),
458 "sequence_expression" => {
460 let mut cursor = node.walk();
461 stack.extend(node.named_children(&mut cursor).last());
462 }
463 "ternary_expression" => stack.extend(
464 [field("consequence"), field("alternative")]
465 .into_iter()
466 .flatten(),
467 ),
468 "binary_expression" => {
469 let preserves = field("operator")
470 .is_some_and(|operator| matches!(node_text(operator, source), "||" | "&&" | "??"));
471 if preserves {
472 stack.extend([field("left"), field("right")].into_iter().flatten());
473 }
474 }
475 "arrow_function" | "function_expression" | "generator_function" | "method_definition" => {
478 match field("body") {
479 Some(body) if body.kind() == "statement_block" => push_returned_values(body, stack),
480 body => stack.extend(body),
481 }
482 }
483 "new_expression" => stack.extend(field("arguments")),
485 _ => {}
486 }
487}
488
489fn push_returned_values<'tree>(body: Node<'tree>, stack: &mut Vec<Node<'tree>>) {
492 let mut pending = vec![body];
493 while let Some(node) = pending.pop() {
494 match node.kind() {
495 "arrow_function"
496 | "function_expression"
497 | "function_declaration"
498 | "generator_function"
499 | "generator_function_declaration"
500 | "method_definition"
501 | "class"
502 | "class_declaration" => continue,
503 "return_statement" | "yield_expression" => {
504 let mut cursor = node.walk();
505 stack.extend(node.named_children(&mut cursor));
506 }
507 _ => {}
508 }
509 let mut cursor = node.walk();
510 pending.extend(node.named_children(&mut cursor));
511 }
512}
513
514fn unresolved_registration_call<'a>(
515 call: Node<'_>,
516 context: &'a AdapterContext<'_, '_>,
517) -> Option<UnresolvedRegistration<'a>> {
518 let function = call.child_by_field_name("function")?;
519 if context
520 .framework_calls
521 .incomplete
522 .contains(&function.start_byte())
523 {
524 return Some(UnresolvedRegistration::Generic);
525 }
526 if context
527 .framework_calls
528 .handled
529 .contains(&function.start_byte())
530 {
531 return None;
532 }
533 if registration_name(function, context.source, context.registrations)
534 .is_some_and(|(_, _, framework)| framework_callbacks::is_new_framework(framework))
535 {
536 return Some(UnresolvedRegistration::Generic);
537 }
538 if registration_name(function, context.source, context.registrations).is_some()
539 || decorator_registration_name(function, context.source, context.registrations).is_some()
540 {
541 return None;
542 }
543 let passes_registration = call
546 .child_by_field_name("arguments")
547 .is_some_and(|arguments| arguments_pass_registration(arguments, context));
548 if passes_registration {
549 return Some(UnresolvedRegistration::Passed);
550 }
551 if let Some(module) =
552 unresolved_registration_module(function, context.source, context.registrations)
553 {
554 return Some(UnresolvedRegistration::Module(module));
555 }
556 if context
557 .framework_calls
558 .inert
559 .contains(&function.start_byte())
560 {
561 return None;
562 }
563 let looks_like_registration = match registration_callee(function, context.source) {
564 Some(RegistrationCallee::Identifier(name)) => matches!(
565 name,
566 "Given"
567 | "When"
568 | "Then"
569 | "And"
570 | "But"
571 | "Step"
572 | "defineStep"
573 | "given"
574 | "when"
575 | "then"
576 ),
577 Some(RegistrationCallee::Property { name, .. }) => matches!(
580 name.as_ref(),
581 "Given" | "When" | "Then" | "And" | "But" | "Step" | "defineStep"
582 ),
583 _ => false,
584 };
585 looks_like_registration.then_some(UnresolvedRegistration::Generic)
586}
587
588fn extract_call<'tree>(
589 call: Node<'tree>,
590 context: &AdapterContext<'_, 'tree>,
591 diagnostics: &mut Vec<ExtractionDiagnostic>,
592) -> Option<StepDefinition> {
593 let source = context.source;
594 let function = call.child_by_field_name("function")?;
595 if context
596 .registrations
597 .forwarded_callees
598 .contains(&function.start_byte())
599 {
600 return None;
601 }
602 let (callee, registration, registration_framework) =
603 context.framework_calls.registration(function).or_else(|| {
604 registration_name(function, source, context.registrations)
605 .filter(|(_, _, framework)| !framework_callbacks::is_new_framework(*framework))
606 })?;
607 let arguments = call.child_by_field_name("arguments")?;
608 let mut cursor = arguments.walk();
609 let arguments: Vec<_> = arguments.named_children(&mut cursor).collect();
610 let Some(&matcher_node) = arguments.first() else {
611 warn_unsupported_matcher(call, context, diagnostics);
612 return None;
613 };
614 let (matcher, mut matcher_kind, matcher_flags) =
615 extract_matcher(matcher_node, context, diagnostics)?;
616 if arguments.len() < 2 {
617 return None;
618 }
619 if registration_framework == Framework::JestCucumber
620 && matcher_kind == MatcherKind::CucumberExpression
621 {
622 matcher_kind = MatcherKind::Literal;
623 }
624 let handler = arguments.iter().rev().copied().find(|node| {
625 matches!(
626 node.kind(),
627 "arrow_function"
628 | "function_expression"
629 | "generator_function"
630 | "identifier"
631 | "member_expression"
632 | "call_expression"
633 )
634 });
635 let Some(handler) = handler else {
636 diagnostics.push(ExtractionDiagnostic {
637 level: ExtractionDiagnosticLevel::Warning,
638 location: node_location(context.file, call, source),
639 message: "dynamic or unsupported step handler cannot be compared statically".to_owned(),
640 });
641 return None;
642 };
643 let bound_arguments = bind_arguments(handler, source);
644 let resolved_handler = resolve_handler(handler, source, context.handler_bindings);
645 if resolved_handler.is_none() && handler.kind() == "call_expression" {
646 diagnostics.push(ExtractionDiagnostic {
647 level: ExtractionDiagnosticLevel::Warning,
648 location: node_location(context.file, handler, source),
649 message: "dynamic or unsupported step handler cannot be compared statically".to_owned(),
650 });
651 }
652 let comparable = resolved_handler.is_some();
653 let handler = resolved_handler.unwrap_or(handler);
654 Some(step_definition(
655 call,
656 context,
657 diagnostics,
658 (callee, registration, registration_framework),
659 (matcher, matcher_kind, matcher_flags),
660 fingerprint_handler(
661 handler,
662 bound_arguments,
663 source,
664 comparable,
665 context.assertions,
666 ),
667 ))
668}
669
670fn extract_decorator<'tree>(
671 call: Node<'tree>,
672 context: &AdapterContext<'_, 'tree>,
673 diagnostics: &mut Vec<ExtractionDiagnostic>,
674) -> Option<StepDefinition> {
675 let source = context.source;
676 let function = call.child_by_field_name("function")?;
677 let (callee, registration, registration_framework) =
678 decorator_registration_name(function, source, context.registrations)?;
679 let arguments = call.child_by_field_name("arguments")?;
681 let mut cursor = arguments.walk();
682 let Some(matcher_node) = arguments.named_children(&mut cursor).next() else {
683 warn_unsupported_matcher(call, context, diagnostics);
684 return None;
685 };
686 let (matcher, matcher_kind, matcher_flags) =
687 extract_matcher(matcher_node, context, diagnostics)?;
688 let Some(method) = decorated_method(call) else {
689 diagnostics.push(ExtractionDiagnostic {
690 level: ExtractionDiagnosticLevel::Warning,
691 location: node_location(context.file, call, source),
692 message: "dynamic or unsupported step handler cannot be compared statically".to_owned(),
693 });
694 return None;
695 };
696 let Some(handler) = fingerprint_method_handler(method, source, context.assertions) else {
697 diagnostics.push(ExtractionDiagnostic {
698 level: ExtractionDiagnosticLevel::Warning,
699 location: node_location(context.file, method, source),
700 message: "dynamic or unsupported step handler cannot be compared statically".to_owned(),
701 });
702 return None;
703 };
704 Some(step_definition(
705 call,
706 context,
707 diagnostics,
708 (callee, registration, registration_framework),
709 (matcher, matcher_kind, matcher_flags),
710 handler,
711 ))
712}
713
714fn step_definition<'tree>(
719 call: Node<'tree>,
720 context: &AdapterContext<'_, 'tree>,
721 diagnostics: &mut Vec<ExtractionDiagnostic>,
722 (callee, registration, registration_framework): (String, String, Framework),
723 (matcher, matcher_kind, matcher_flags): (String, MatcherKind, String),
724 handler: HandlerFingerprint,
725) -> StepDefinition {
726 StepDefinition {
727 normalized_matcher: normalize_matcher_with_flags(&matcher, matcher_kind, &matcher_flags),
728 matcher,
729 matcher_kind,
730 matcher_flags,
731 handler,
732 framework: effective_registration_framework(registration_framework, context.framework),
733 registration: if registration.is_empty() {
734 callee
735 } else {
736 registration
737 },
738 location: node_location(context.file, call, context.source),
739 inline_suppressions: inline_suppressions(
740 call,
741 context.source_lines,
742 context.file,
743 diagnostics,
744 ),
745 }
746}
747
748fn decorated_method(call: Node<'_>) -> Option<Node<'_>> {
752 let decorator = call
753 .parent()
754 .filter(|parent| parent.kind() == "decorator")?;
755 if let Some(method) = decorator
756 .parent()
757 .filter(|parent| parent.kind() == "method_definition")
758 {
759 return Some(method);
760 }
761 let mut sibling = decorator.next_named_sibling()?;
762 while sibling.kind() == "decorator" {
763 sibling = sibling.next_named_sibling()?;
764 }
765 (sibling.kind() == "method_definition").then_some(sibling)
766}
767
768fn effective_registration_framework(binding: Framework, file: Framework) -> Framework {
769 if binding == Framework::Unknown {
770 file
771 } else {
772 binding
773 }
774}
775
776fn warn_unsupported_matcher(
777 node: Node<'_>,
778 context: &AdapterContext<'_, '_>,
779 diagnostics: &mut Vec<ExtractionDiagnostic>,
780) {
781 diagnostics.push(ExtractionDiagnostic::new(
782 ExtractionDiagnosticLevel::Warning,
783 node_location(context.file, node, context.source),
784 UNSUPPORTED_MATCHER_DIAGNOSTIC,
785 ));
786}
787
788fn extract_matcher(
789 matcher_node: Node<'_>,
790 context: &AdapterContext<'_, '_>,
791 diagnostics: &mut Vec<ExtractionDiagnostic>,
792) -> Option<(String, MatcherKind, String)> {
793 let source = context.source;
794 let (matcher, matcher_kind, matcher_flags) = match matcher_value(matcher_node, source) {
795 Some(value) => value,
796 None if matcher_node.kind() == "string" => {
797 diagnostics.push(ExtractionDiagnostic {
798 level: ExtractionDiagnosticLevel::Error,
799 location: node_location(context.file, matcher_node, source),
800 message: INVALID_MATCHER_DIAGNOSTIC.to_owned(),
801 });
802 return None;
803 }
804 None => {
805 warn_unsupported_matcher(matcher_node, context, diagnostics);
806 return None;
807 }
808 };
809 if matcher_kind == MatcherKind::RegularExpression {
810 match rust_regex_support(&matcher, &matcher_flags) {
811 RegexSupport::Unsupported => diagnostics.push(ExtractionDiagnostic {
812 level: ExtractionDiagnosticLevel::Warning,
813 location: node_location(context.file, matcher_node, source),
814 message: "regular expression uses syntax unsupported by static usage analysis; unused and ambiguity checks will treat this definition as indeterminate".to_owned(),
815 }),
816 RegexSupport::ResourceLimit | RegexSupport::Supported => {}
818 }
819 }
820 Some((matcher, matcher_kind, matcher_flags))
821}
822
823fn node_location(file: &SourceFile, node: Node<'_>, source: &[u8]) -> SourceLocation {
824 let start = node.start_position();
825 let end = node.end_position();
826 SourceLocation::new(
827 &file.path,
828 start.row + 1,
829 unicode_column(source, node.start_byte(), start.column),
830 end.row + 1,
831 unicode_column(source, node.end_byte(), end.column),
832 )
833}
834
835fn unicode_column(source: &[u8], byte_offset: usize, byte_column: usize) -> usize {
836 let line_start = byte_offset.saturating_sub(byte_column);
837 let end = byte_offset.min(source.len());
838 std::str::from_utf8(&source[line_start.min(end)..end])
839 .map_or(byte_column, |prefix| prefix.chars().count())
840 + 1
841}
842
843fn first_syntax_error(root: Node<'_>) -> Option<Node<'_>> {
844 let mut stack = vec![root];
845 while let Some(node) = stack.pop() {
846 if node.is_error() || node.is_missing() {
847 return Some(node);
848 }
849 let mut cursor = node.walk();
850 let children: Vec<_> = node.named_children(&mut cursor).collect();
851 stack.extend(children.into_iter().rev());
852 }
853 None
854}
855
856fn node_text<'a>(node: Node<'_>, source: &'a [u8]) -> &'a str {
857 std::str::from_utf8(&source[node.byte_range()]).unwrap_or("")
858}
859
860#[cfg(test)]
861mod tests;