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