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