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