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