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,
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}
161
162#[derive(Default)]
163struct UnresolvedRegistrationCalls {
164 count: usize,
165 first_location: Option<SourceLocation>,
166 modules: BTreeMap<String, (usize, SourceLocation)>,
167 passed: usize,
169 first_passed: Option<SourceLocation>,
170}
171
172impl UnresolvedRegistrationCalls {
173 fn record(&mut self, location: SourceLocation) {
174 self.count = self.count.saturating_add(1);
175 self.first_location.get_or_insert(location);
176 }
177
178 fn record_passed(&mut self, location: SourceLocation) {
179 self.passed = self.passed.saturating_add(1);
180 self.first_passed.get_or_insert(location);
181 }
182
183 fn record_module(&mut self, module: &str, location: SourceLocation) {
184 let entry = self
185 .modules
186 .entry(module.to_owned())
187 .or_insert_with(|| (0, location));
188 entry.0 = entry.0.saturating_add(1);
189 }
190}
191
192pub fn extract_file(file: &SourceFile) -> Result<Vec<StepDefinition>> {
194 Ok(extract_file_detailed(file)?.definitions)
195}
196
197pub fn extract_file_detailed(file: &SourceFile) -> Result<Extraction> {
199 adapter_for_language(file.language).extract_file(file)
200}
201
202pub fn extract(source: &str, file: &SourceFile) -> Result<Vec<StepDefinition>> {
204 Ok(extract_detailed(source, file)?.definitions)
205}
206
207pub fn extract_detailed(source: &str, file: &SourceFile) -> Result<Extraction> {
209 adapter_for_language(file.language).extract(source, file)
210}
211
212fn extract_detailed_impl(
213 source: &str,
214 file: &SourceFile,
215 session: &mut TypeScriptExtractionSession,
216) -> Result<Extraction> {
217 let mut parser = Parser::new();
218 parser
219 .set_language(&grammar_for_language(file.language))
220 .context("failed to initialize the JavaScript/TypeScript parser")?;
221 let tree = parser
222 .parse(source, None)
223 .context("Tree-sitter did not return a syntax tree")?;
224
225 let root = tree.root_node();
226 let source_bytes = source.as_bytes();
227 let source_lines: Vec<_> = source.lines().collect();
228 let framework = detect_framework(root, source_bytes);
229 let registrations = detect_registrations(
230 root,
231 source_bytes,
232 framework,
233 &file.path,
234 &mut session.resolver,
235 &session.configured_registrations,
236 )?;
237 let framework = registrations.framework;
238 let framework_calls = framework_callbacks::discover(
239 root,
240 source_bytes,
241 &file.path,
242 &mut session.resolver,
243 ®istrations,
244 )?;
245 let assertions = AssertionBindings::discover(
246 root,
247 source_bytes,
248 ®istrations,
249 &session.configured_assertion_modules,
250 );
251 let mut handler_bindings = BTreeMap::new();
252 collect_handler_bindings(root, source_bytes, &mut handler_bindings);
253 let context = AdapterContext {
254 source: source_bytes,
255 source_lines: &source_lines,
256 file,
257 framework,
258 registrations: ®istrations,
259 framework_calls: &framework_calls,
260 assertions: &assertions,
261 handler_bindings: &handler_bindings,
262 };
263 let mut definitions = Vec::new();
264 let mut diagnostics = Vec::new();
265 let mut unresolved_registration_calls = UnresolvedRegistrationCalls::default();
266 if root.has_error() {
267 let syntax_node = first_syntax_error(root).unwrap_or(root);
268 diagnostics.push(ExtractionDiagnostic {
273 kind: ExtractionDiagnosticKind::Unparseable,
274 level: ExtractionDiagnosticLevel::Warning,
275 location: node_location(file, syntax_node, source_bytes),
276 message: format!("{UNPARSEABLE_SOURCE_DIAGNOSTIC_PREFIX}, so analysis is incomplete; fix the syntax, use a .tsx extension for JSX, or narrow definition discovery"),
277 });
278 }
279 collect_calls(
280 root,
281 &context,
282 &mut definitions,
283 &mut diagnostics,
284 &mut unresolved_registration_calls,
285 );
286 if let Some(location) = unresolved_registration_calls.first_location {
287 diagnostics.push(ExtractionDiagnostic {
288 kind: ExtractionDiagnosticKind::Incomplete,
289 level: ExtractionDiagnosticLevel::Warning,
290 location,
291 message: format!(
292 "{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",
293 unresolved_registration_calls.count
294 ),
295 });
296 }
297 if let Some(location) = unresolved_registration_calls.first_passed {
298 diagnostics.push(ExtractionDiagnostic {
299 kind: ExtractionDiagnosticKind::Incomplete,
300 level: ExtractionDiagnosticLevel::Warning,
301 location,
302 message: format!(
303 "{UNRESOLVED_REGISTRATION_DIAGNOSTIC_PREFIX} {} call(s) pass a step registration to a function the analyzer does not model; definitions may be missing",
304 unresolved_registration_calls.passed
305 ),
306 });
307 }
308 let mut explained_modules = BTreeSet::new();
309 for (module, (count, location)) in unresolved_registration_calls.modules {
310 let cause = registrations::unresolved_module_reason(®istrations, &module)
311 .map(|reason| format!(" ({reason})"))
312 .unwrap_or_default();
313 diagnostics.push(ExtractionDiagnostic {
314 kind: ExtractionDiagnosticKind::Incomplete,
315 level: ExtractionDiagnosticLevel::Warning,
316 location,
317 message: format!(
318 "{UNRESOLVED_REGISTRATION_DIAGNOSTIC_PREFIX} {count} call(s) import a known registration through unresolved module `{module}`{cause}; definitions may be missing"
319 ),
320 });
321 explained_modules.insert(module);
322 }
323 for (module, recorded) in registrations::unresolved_module_reasons(®istrations) {
328 if explained_modules.contains(module) {
329 continue;
330 }
331 let line = recorded.row + 1;
332 let column = unicode_column(source_bytes, recorded.byte_offset, recorded.byte_column);
333 diagnostics.push(ExtractionDiagnostic {
334 kind: ExtractionDiagnosticKind::Incomplete,
335 level: ExtractionDiagnosticLevel::Warning,
336 location: SourceLocation::new(&file.path, line, column, line, column),
337 message: format!(
338 "{UNRESOLVED_REGISTRATION_DIAGNOSTIC_PREFIX} module `{module}` could not be resolved statically ({}); definitions may be missing",
339 recorded.reason
340 ),
341 });
342 }
343 definitions.sort_by(|left, right| {
344 left.location
345 .line
346 .cmp(&right.location.line)
347 .then(left.location.column.cmp(&right.location.column))
348 });
349 Ok(Extraction::new(definitions, diagnostics))
350}
351
352fn collect_calls<'tree>(
353 node: Node<'tree>,
354 context: &AdapterContext<'_, 'tree>,
355 definitions: &mut Vec<StepDefinition>,
356 diagnostics: &mut Vec<ExtractionDiagnostic>,
357 unresolved_registration_calls: &mut UnresolvedRegistrationCalls,
358) {
359 let mut stack = vec![node];
360 while let Some(node) = stack.pop() {
361 if node.kind() == "call_expression" {
362 if let Some(unresolved) = unresolved_registration_call(node, context) {
363 let location = node_location(context.file, node, context.source);
364 match unresolved {
365 UnresolvedRegistration::Generic => {
366 unresolved_registration_calls.record(location)
367 }
368 UnresolvedRegistration::Module(module) => {
369 unresolved_registration_calls.record_module(module, location)
370 }
371 UnresolvedRegistration::Passed => {
372 unresolved_registration_calls.record_passed(location)
373 }
374 }
375 }
376 let definition = if node
377 .parent()
378 .is_some_and(|parent| parent.kind() == "decorator")
379 {
380 extract_decorator(node, context, diagnostics)
381 } else {
382 extract_call(node, context, diagnostics)
383 };
384 if let Some(definition) = definition {
385 definitions.push(definition);
386 }
387 }
388 let mut cursor = node.walk();
389 let children: Vec<_> = node.named_children(&mut cursor).collect();
390 stack.extend(children.into_iter().rev());
391 }
392}
393
394enum UnresolvedRegistration<'a> {
395 Generic,
396 Module(&'a str),
397 Passed,
399}
400
401fn arguments_pass_registration(arguments: Node<'_>, context: &AdapterContext<'_, '_>) -> bool {
408 let mut stack = vec![arguments];
409 while let Some(node) = stack.pop() {
410 let passed = match node.kind() {
411 "call_expression" => {
414 let bound = node
415 .child_by_field_name("function")
416 .filter(|function| function.kind() == "member_expression")
417 .filter(|function| {
418 function
419 .child_by_field_name("property")
420 .is_some_and(|property| node_text(property, context.source) == "bind")
421 })
422 .and_then(|function| function.child_by_field_name("object"));
423 stack.extend(bound);
424 continue;
425 }
426 "identifier" | "member_expression" | "subscript_expression" => {
427 registration_name(node, context.source, context.registrations).is_some()
428 || decorator_registration_name(node, context.source, context.registrations)
429 .is_some()
430 }
431 "shorthand_property_identifier" => context
433 .registrations
434 .recognizes_alias_at(node, node_text(node, context.source)),
435 _ => false,
436 };
437 if passed {
438 return true;
439 }
440 push_value_positions(node, context.source, &mut stack);
441 }
442 false
443}
444
445fn push_value_positions<'tree>(node: Node<'tree>, source: &[u8], stack: &mut Vec<Node<'tree>>) {
451 let field = |name: &str| node.child_by_field_name(name);
452 match node.kind() {
453 "arguments"
454 | "array"
455 | "object"
456 | "parenthesized_expression"
457 | "as_expression"
458 | "satisfies_expression"
459 | "non_null_expression"
460 | "await_expression" => {
461 let mut cursor = node.walk();
462 stack.extend(node.named_children(&mut cursor));
463 }
464 "pair" | "assignment_expression" => stack.extend(field("value").or(field("right"))),
465 "spread_element" => stack.extend(
468 node.named_child(0)
469 .filter(|operand| matches!(operand.kind(), "array" | "object")),
470 ),
471 "sequence_expression" => {
473 let mut cursor = node.walk();
474 stack.extend(node.named_children(&mut cursor).last());
475 }
476 "ternary_expression" => stack.extend(
477 [field("consequence"), field("alternative")]
478 .into_iter()
479 .flatten(),
480 ),
481 "binary_expression" => {
482 let preserves = field("operator")
483 .is_some_and(|operator| matches!(node_text(operator, source), "||" | "&&" | "??"));
484 if preserves {
485 stack.extend([field("left"), field("right")].into_iter().flatten());
486 }
487 }
488 "arrow_function" | "function_expression" | "generator_function" | "method_definition" => {
491 match field("body") {
492 Some(body) if body.kind() == "statement_block" => push_returned_values(body, stack),
493 body => stack.extend(body),
494 }
495 }
496 "new_expression" => stack.extend(field("arguments")),
498 _ => {}
499 }
500}
501
502fn push_returned_values<'tree>(body: Node<'tree>, stack: &mut Vec<Node<'tree>>) {
505 let mut pending = vec![body];
506 while let Some(node) = pending.pop() {
507 match node.kind() {
508 "arrow_function"
509 | "function_expression"
510 | "function_declaration"
511 | "generator_function"
512 | "generator_function_declaration"
513 | "method_definition"
514 | "class"
515 | "class_declaration" => continue,
516 "return_statement" | "yield_expression" => {
517 let mut cursor = node.walk();
518 stack.extend(node.named_children(&mut cursor));
519 }
520 _ => {}
521 }
522 let mut cursor = node.walk();
523 pending.extend(node.named_children(&mut cursor));
524 }
525}
526
527fn unresolved_registration_call<'a>(
528 call: Node<'_>,
529 context: &'a AdapterContext<'_, '_>,
530) -> Option<UnresolvedRegistration<'a>> {
531 let function = call.child_by_field_name("function")?;
532 if context
533 .framework_calls
534 .incomplete
535 .contains(&function.start_byte())
536 {
537 return Some(UnresolvedRegistration::Generic);
538 }
539 if context
540 .framework_calls
541 .handled
542 .contains(&function.start_byte())
543 {
544 return None;
545 }
546 if registration_name(function, context.source, context.registrations)
547 .is_some_and(|(_, _, framework)| framework_callbacks::is_new_framework(framework))
548 {
549 return Some(UnresolvedRegistration::Generic);
550 }
551 if registration_name(function, context.source, context.registrations).is_some()
552 || decorator_registration_name(function, context.source, context.registrations).is_some()
553 {
554 return None;
555 }
556 let passes_registration = call
559 .child_by_field_name("arguments")
560 .is_some_and(|arguments| arguments_pass_registration(arguments, context));
561 if passes_registration {
562 return Some(UnresolvedRegistration::Passed);
563 }
564 if let Some(module) =
565 unresolved_registration_module(function, context.source, context.registrations)
566 {
567 return Some(UnresolvedRegistration::Module(module));
568 }
569 if context
570 .framework_calls
571 .inert
572 .contains(&function.start_byte())
573 {
574 return None;
575 }
576 let looks_like_registration = match registration_callee(function, context.source) {
577 Some(RegistrationCallee::Identifier(name)) => matches!(
578 name,
579 "Given"
580 | "When"
581 | "Then"
582 | "And"
583 | "But"
584 | "Step"
585 | "defineStep"
586 | "given"
587 | "when"
588 | "then"
589 ),
590 Some(RegistrationCallee::Property { name, .. }) => matches!(
593 name.as_ref(),
594 "Given" | "When" | "Then" | "And" | "But" | "Step" | "defineStep"
595 ),
596 _ => false,
597 };
598 looks_like_registration.then_some(UnresolvedRegistration::Generic)
599}
600
601fn extract_call<'tree>(
602 call: Node<'tree>,
603 context: &AdapterContext<'_, 'tree>,
604 diagnostics: &mut Vec<ExtractionDiagnostic>,
605) -> Option<StepDefinition> {
606 let source = context.source;
607 let function = call.child_by_field_name("function")?;
608 if context
609 .registrations
610 .forwarded_callees
611 .contains(&function.start_byte())
612 {
613 return None;
614 }
615 let (callee, registration, registration_framework) =
616 context.framework_calls.registration(function).or_else(|| {
617 registration_name(function, source, context.registrations)
618 .filter(|(_, _, framework)| !framework_callbacks::is_new_framework(*framework))
619 })?;
620 let arguments = call.child_by_field_name("arguments")?;
621 let mut cursor = arguments.walk();
622 let arguments: Vec<_> = arguments.named_children(&mut cursor).collect();
623 let Some(&matcher_node) = arguments.first() else {
624 warn_unsupported_matcher(call, context, diagnostics);
625 return None;
626 };
627 let (matcher, mut matcher_kind, matcher_flags) =
628 extract_matcher(matcher_node, context, diagnostics)?;
629 if arguments.len() < 2 {
630 return None;
631 }
632 if registration_framework == Framework::JestCucumber
633 && matcher_kind == MatcherKind::CucumberExpression
634 {
635 matcher_kind = MatcherKind::Literal;
636 }
637 let handler = arguments.iter().rev().copied().find(|node| {
638 matches!(
639 node.kind(),
640 "arrow_function"
641 | "function_expression"
642 | "generator_function"
643 | "identifier"
644 | "member_expression"
645 | "call_expression"
646 )
647 });
648 let Some(handler) = handler else {
649 diagnostics.push(unsupported_handler_diagnostic(context, call));
650 return None;
651 };
652 let bound_arguments = bind_arguments(handler, source);
653 let resolved_handler = resolve_handler(handler, source, context.handler_bindings);
654 if resolved_handler.is_none() && handler.kind() == "call_expression" {
655 diagnostics.push(unsupported_handler_diagnostic(context, handler));
656 }
657 let comparable = resolved_handler.is_some();
658 let handler = resolved_handler.unwrap_or(handler);
659 Some(step_definition(
660 call,
661 context,
662 diagnostics,
663 (callee, registration, registration_framework),
664 (matcher, matcher_kind, matcher_flags),
665 fingerprint_handler(
666 handler,
667 bound_arguments,
668 source,
669 comparable,
670 context.assertions,
671 ),
672 ))
673}
674
675fn extract_decorator<'tree>(
676 call: Node<'tree>,
677 context: &AdapterContext<'_, 'tree>,
678 diagnostics: &mut Vec<ExtractionDiagnostic>,
679) -> Option<StepDefinition> {
680 let source = context.source;
681 let function = call.child_by_field_name("function")?;
682 let (callee, registration, registration_framework) =
683 decorator_registration_name(function, source, context.registrations)?;
684 let arguments = call.child_by_field_name("arguments")?;
686 let mut cursor = arguments.walk();
687 let Some(matcher_node) = arguments.named_children(&mut cursor).next() else {
688 warn_unsupported_matcher(call, context, diagnostics);
689 return None;
690 };
691 let (matcher, matcher_kind, matcher_flags) =
692 extract_matcher(matcher_node, context, diagnostics)?;
693 let Some(method) = decorated_method(call) else {
694 diagnostics.push(unsupported_handler_diagnostic(context, call));
695 return None;
696 };
697 let Some(handler) = fingerprint_method_handler(method, source, context.assertions) else {
698 diagnostics.push(unsupported_handler_diagnostic(context, method));
699 return None;
700 };
701 Some(step_definition(
702 call,
703 context,
704 diagnostics,
705 (callee, registration, registration_framework),
706 (matcher, matcher_kind, matcher_flags),
707 handler,
708 ))
709}
710
711fn step_definition<'tree>(
716 call: Node<'tree>,
717 context: &AdapterContext<'_, 'tree>,
718 diagnostics: &mut Vec<ExtractionDiagnostic>,
719 (callee, registration, registration_framework): (String, String, Framework),
720 (matcher, matcher_kind, matcher_flags): (String, MatcherKind, String),
721 handler: HandlerFingerprint,
722) -> StepDefinition {
723 StepDefinition {
724 normalized_matcher: normalize_matcher_with_flags(&matcher, matcher_kind, &matcher_flags),
725 matcher,
726 matcher_kind,
727 matcher_flags,
728 handler,
729 framework: effective_registration_framework(registration_framework, context.framework),
730 registration: if registration.is_empty() {
731 callee
732 } else {
733 registration
734 },
735 location: node_location(context.file, call, context.source),
736 inline_suppressions: inline_suppressions(
737 call.start_position().row,
738 "//",
739 context.source_lines,
740 context.file,
741 diagnostics,
742 ),
743 }
744}
745
746fn decorated_method(call: Node<'_>) -> Option<Node<'_>> {
750 let decorator = call
751 .parent()
752 .filter(|parent| parent.kind() == "decorator")?;
753 if let Some(method) = decorator
754 .parent()
755 .filter(|parent| parent.kind() == "method_definition")
756 {
757 return Some(method);
758 }
759 let mut sibling = decorator.next_named_sibling()?;
760 while sibling.kind() == "decorator" {
761 sibling = sibling.next_named_sibling()?;
762 }
763 (sibling.kind() == "method_definition").then_some(sibling)
764}
765
766fn effective_registration_framework(binding: Framework, file: Framework) -> Framework {
767 if binding == Framework::Unknown {
768 file
769 } else {
770 binding
771 }
772}
773
774fn warn_unsupported_matcher(
775 node: Node<'_>,
776 context: &AdapterContext<'_, '_>,
777 diagnostics: &mut Vec<ExtractionDiagnostic>,
778) {
779 diagnostics.push(ExtractionDiagnostic::with_kind(
780 ExtractionDiagnosticKind::Incomplete,
781 ExtractionDiagnosticLevel::Warning,
782 node_location(context.file, node, context.source),
783 UNSUPPORTED_MATCHER_DIAGNOSTIC,
784 ));
785}
786
787fn extract_matcher(
788 matcher_node: Node<'_>,
789 context: &AdapterContext<'_, '_>,
790 diagnostics: &mut Vec<ExtractionDiagnostic>,
791) -> Option<(String, MatcherKind, String)> {
792 let source = context.source;
793 let (matcher, matcher_kind, matcher_flags) = match matcher_value(matcher_node, source) {
794 Some(value) => value,
795 None if matcher_node.kind() == "string" => {
796 diagnostics.push(ExtractionDiagnostic {
797 kind: ExtractionDiagnosticKind::Incomplete,
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 kind: ExtractionDiagnosticKind::Other,
813 level: ExtractionDiagnosticLevel::Warning,
814 location: node_location(context.file, matcher_node, source),
815 message: "regular expression uses syntax unsupported by static usage analysis; unused and ambiguity checks will treat this definition as indeterminate".to_owned(),
816 }),
817 RegexSupport::ResourceLimit | RegexSupport::Supported => {}
819 }
820 }
821 Some((matcher, matcher_kind, matcher_flags))
822}
823
824fn unsupported_handler_diagnostic(
825 context: &AdapterContext<'_, '_>,
826 node: Node<'_>,
827) -> ExtractionDiagnostic {
828 ExtractionDiagnostic::with_kind(
829 ExtractionDiagnosticKind::Other,
830 ExtractionDiagnosticLevel::Warning,
831 node_location(context.file, node, context.source),
832 "dynamic or unsupported step handler cannot be compared statically",
833 )
834}
835
836fn node_location(file: &SourceFile, node: Node<'_>, source: &[u8]) -> SourceLocation {
837 let start = node.start_position();
838 let end = node.end_position();
839 SourceLocation::new(
840 &file.path,
841 start.row + 1,
842 unicode_column(source, node.start_byte(), start.column),
843 end.row + 1,
844 unicode_column(source, node.end_byte(), end.column),
845 )
846}
847
848fn unicode_column(source: &[u8], byte_offset: usize, byte_column: usize) -> usize {
849 let line_start = byte_offset.saturating_sub(byte_column);
850 let end = byte_offset.min(source.len());
851 std::str::from_utf8(&source[line_start.min(end)..end])
852 .map_or(byte_column, |prefix| prefix.chars().count())
853 + 1
854}
855
856fn first_syntax_error(root: Node<'_>) -> Option<Node<'_>> {
857 let mut stack = vec![root];
858 while let Some(node) = stack.pop() {
859 if node.is_error() || node.is_missing() {
860 return Some(node);
861 }
862 let mut cursor = node.walk();
863 let children: Vec<_> = node.named_children(&mut cursor).collect();
864 stack.extend(children.into_iter().rev());
865 }
866 None
867}
868
869fn node_text<'a>(node: Node<'_>, source: &'a [u8]) -> &'a str {
870 std::str::from_utf8(&source[node.byte_range()]).unwrap_or("")
871}
872
873#[cfg(test)]
874mod tests;