#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
use crate::token_role::TokenRole;
use crate::lang_helpers::tcl_family::{
ArgumentRoles, Dialect, SWITCH_COMMAND, argument_slots_are_readable, fills_script_slot,
namespace_script_slots, script_slots,
};
use crate::space_kind::SpaceKind;
use crate::traits::Search;
use crate::*;
#[inline]
pub(crate) fn node_text<'a>(code: &'a [u8], node: &Node) -> Option<&'a str> {
code.get(node.start_byte()..node.end_byte())
.and_then(|bytes| std::str::from_utf8(bytes).ok())
}
macro_rules! get_operator {
($language:ident) => {
#[inline]
fn get_operator_id_as_str(id: u16) -> &'static str {
let typ = id.into();
match typ {
$language::LPAREN => "()",
$language::LBRACK => "[]",
$language::LBRACE => "{}",
_ => typ.into(),
}
}
};
}
macro_rules! impl_js_family_get_op_type {
(
$lang:ident,
op_extras: [$($op_extra:ident),* $(,)?],
operand_extras: [$($operand_extra:ident),* $(,)?]
$(, predefined_void: $predefined_type:ident)? $(,)?
) => {
fn get_op_type<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> TokenRole {
use $lang::*;
$(
if node.kind_id() == $predefined_type as u16
&& node
.child(0)
.is_some_and(|child| child.kind_id() == Void as u16)
{
return TokenRole::Unknown;
}
)?
match node.kind_id().into() {
SLASH
if ancestors.parent_has_kind(node, Regex as u16) =>
{
TokenRole::Unknown
}
Export | Import | Import2 | Extends | DOT | From | LPAREN | COMMA | As | STAR
| GTGT | GTGTGT | COLON | Return | Delete | Throw | Break | Continue | If
| Else | Switch | Case | Default | Async | Do | For | In | Of | While | Try
| Catch | Finally | With | EQ | AT | AMPAMP | PIPEPIPE | PLUS | DASH | DASHDASH
| PLUSPLUS | SLASH | PERCENT | STARSTAR | PIPE | AMP | LTLT | TILDE | LT | LTEQ
| EQEQ | BANGEQ | GTEQ | GT | PLUSEQ | BANG | BANGEQEQ | EQEQEQ | DASHEQ
| STAREQ | SLASHEQ | PERCENTEQ | STARSTAREQ | GTGTEQ | GTGTGTEQ | LTLTEQ | AMPEQ
| CARET | CARETEQ | PIPEEQ | Yield | LBRACK | LBRACE | Await | QMARK
| QMARKQMARK | EQGT | DOTDOTDOT | New | Let | Var | Const | Function
| FunctionExpression | SEMI | Typeof | Instanceof | Void
| Set | Get
$(| $op_extra)* => TokenRole::Operator,
Identifier | PropertyIdentifier | PrivatePropertyIdentifier | MetaProperty
| String | Number | True | False | Null | This | Super | Undefined | Regex
$(| $operand_extra)* => TokenRole::Operand,
TemplateString => {
Self::string_operand_type(node, &[TemplateSubstitution as u16])
}
_ => TokenRole::Unknown,
}
}
};
}
#[must_use]
pub fn default_func_space_name<'a, 'tree>(
node: &Node<'tree>,
code: &'a [u8],
_ancestors: Ancestors<'tree, '_>,
) -> Option<&'a str> {
if let Some(name) = node.child_by_field_name("name") {
node_text(code, &name)
} else {
Some("<anonymous>")
}
}
#[doc(hidden)]
pub struct BracedWordKinds {
pub value: u16,
pub script: u16,
pub comment: u16,
pub command: u16,
pub word_list: u16,
pub simple_word: u16,
pub argument: u16,
pub procedure: u16,
pub namespace: u16,
pub open_brace: u16,
}
pub trait Getter {
#[must_use]
fn get_func_name<'a, 'tree>(
node: &Node<'tree>,
code: &'a [u8],
ancestors: Ancestors<'tree, '_>,
) -> Option<&'a str> {
Self::get_func_space_name(node, code, ancestors)
}
#[must_use]
fn get_func_space_name<'a, 'tree>(
node: &Node<'tree>,
code: &'a [u8],
ancestors: Ancestors<'tree, '_>,
) -> Option<&'a str> {
default_func_space_name(node, code, ancestors)
}
#[must_use]
fn get_space_kind(_node: &Node) -> SpaceKind {
SpaceKind::Unknown
}
#[inline]
#[must_use]
fn get_space_kind_with_code<'a>(
node: &Node<'a>,
_code: &[u8],
_ancestors: Ancestors<'a, '_>,
) -> SpaceKind {
Self::get_space_kind(node)
}
#[must_use]
fn get_op_type<'a>(_node: &Node<'a>, _ancestors: Ancestors<'a, '_>) -> TokenRole {
TokenRole::Unknown
}
#[inline]
#[must_use]
fn get_op_type_with_code<'a>(
node: &Node<'a>,
_code: &[u8],
ancestors: Ancestors<'a, '_>,
) -> TokenRole {
Self::get_op_type(node, ancestors)
}
#[inline]
#[must_use]
fn get_operand_id<'a>(
node: &Node<'a>,
code: &'a [u8],
_ancestors: Ancestors<'a, '_>,
) -> &'a [u8] {
&code[node.start_byte()..node.end_byte()]
}
#[must_use]
fn string_operand_type(node: &Node, interp_kinds: &[u16]) -> TokenRole {
if node.wraps_any(interp_kinds) {
TokenRole::Unknown
} else {
TokenRole::Operand
}
}
#[must_use]
fn is_subsumed_braced_word<'a>(
node: &Node<'a>,
ancestors: Ancestors<'a, '_>,
kinds: &BracedWordKinds,
) -> bool {
ancestors.parent_has_kind(node, kinds.value)
|| (node.kind_id() == kinds.script
&& node
.children()
.any(|child| child.is_named() && child.kind_id() != kinds.comment))
}
#[must_use]
fn generic_argument_command<'tree, 'chain>(
word: &Node<'tree>,
ancestors: Ancestors<'tree, 'chain>,
kinds: &BracedWordKinds,
) -> Option<(Node<'tree>, Ancestors<'tree, 'chain>)> {
let mut chain = ancestors.iter(word);
let (parent, _) = chain.next()?;
if parent.kind_id() != kinds.word_list {
return None;
}
let (grandparent, grandparent_ancestors) = chain.next()?;
(grandparent.kind_id() == kinds.command).then_some((grandparent, grandparent_ancestors))
}
#[must_use]
fn is_value_braced_word<'a>(
word: &Node<'a>,
code: &[u8],
ancestors: Ancestors<'a, '_>,
kinds: &BracedWordKinds,
) -> bool {
if ancestors
.parent(word)
.is_some_and(|parent| [kinds.argument, kinds.command].contains(&parent.kind_id()))
{
return true;
}
let Some((command, command_ancestors)) =
Self::generic_argument_command(word, ancestors, kinds)
else {
return false;
};
if Self::command_leading_word(&command, code, kinds)
.and_then(script_slots)
.is_some()
{
return false;
}
!(Self::is_switch_arm(&command, code, command_ancestors, kinds)
&& Self::is_switch_arm_body(word, &command))
}
#[must_use]
fn is_braced_script_word<'a>(
node: &Node<'a>,
code: &[u8],
ancestors: Ancestors<'a, '_>,
kinds: &BracedWordKinds,
) -> bool {
node.kind_id() == kinds.script
&& !Self::is_value_braced_word(node, code, ancestors, kinds)
&& !Self::is_braced_literal_slot(node, code, ancestors, kinds)
}
#[must_use]
fn is_braced_literal_slot<'a>(
word: &Node<'a>,
code: &[u8],
ancestors: Ancestors<'a, '_>,
kinds: &BracedWordKinds,
) -> bool {
let mut chain = ancestors.iter(word);
let Some((parent, above_parent)) = chain.next() else {
return false;
};
if parent.kind_id() == kinds.procedure {
let is_name =
matches!(parent.child_by_field_name("name"), Some(name) if name.id() == word.id());
return is_name && !Self::is_switch_arm(&parent, code, above_parent, kinds);
}
let Some((owner, above_owner)) = chain.next() else {
return false;
};
if parent.kind_id() != kinds.word_list
|| !argument_slots_are_readable(&owner, &parent)
|| Self::is_switch_arm(&owner, code, above_owner, kinds)
{
return false;
}
let dialect = Dialect { code, kinds };
let roles = if owner.kind_id() == kinds.namespace {
namespace_script_slots(&parent, dialect).map(|slots| ArgumentRoles { slots, first: 1 })
} else if owner.kind_id() == kinds.command {
Self::command_leading_word(&owner, code, kinds)
.and_then(script_slots)
.map(|slots| ArgumentRoles { slots, first: 0 })
} else {
None
};
roles.is_some_and(|roles| !fills_script_slot(word, &parent, roles, dialect))
}
#[must_use]
fn is_switch_arm_body(word: &Node<'_>, command: &Node<'_>) -> bool {
let Some(arguments) = command.child_by_field_name("arguments") else {
return false;
};
let mut cursor = arguments.cursor();
let index = cursor.goto_first_child_for_byte(word.start_byte());
cursor.node().id() == word.id() && matches!(index, Some(index) if index.is_multiple_of(2))
}
#[must_use]
fn command_leading_word<'c>(
command: &Node<'_>,
code: &'c [u8],
kinds: &BracedWordKinds,
) -> Option<&'c str> {
let name = command.child_by_field_name("name")?;
if name.kind_id() != kinds.simple_word {
return None;
}
let text = node_text(code, &name)?;
Some(crate::lang_helpers::strip_global_qualifier(text))
}
#[must_use]
fn is_switch_arm<'a>(
command: &Node<'a>,
code: &[u8],
ancestors: Ancestors<'a, '_>,
kinds: &BracedWordKinds,
) -> bool {
ancestors
.iter(command)
.next()
.is_some_and(|(arm_list, above)| {
arm_list.kind_id() == kinds.script
&& Self::generic_argument_command(&arm_list, above, kinds)
.and_then(|(switch, _)| Self::command_leading_word(&switch, code, kinds))
== Some(SWITCH_COMMAND)
})
}
#[must_use]
fn braced_word_op_type<'a>(
node: &Node<'a>,
code: &[u8],
ancestors: Ancestors<'a, '_>,
kinds: &BracedWordKinds,
) -> TokenRole {
let base = Self::get_op_type(node, ancestors);
if node.kind_id() != kinds.open_brace || !matches!(base, TokenRole::Operator) {
return base;
}
let quotes_a_literal = ancestors.iter(node).next().is_some_and(|(parent, above)| {
parent.kind_id() == kinds.script
&& !Self::is_braced_script_word(&parent, code, above, kinds)
});
if quotes_a_literal {
TokenRole::Unknown
} else {
base
}
}
#[must_use]
fn get_operator_id_as_str(_id: u16) -> &'static str {
""
}
}
mod bash;
mod c;
mod ccomment;
mod cpp;
mod csharp;
mod elixir;
mod go;
mod groovy;
mod irules;
mod java;
mod javascript;
mod kotlin;
mod lua;
mod mozcpp;
mod mozjs;
mod objc;
mod perl;
mod php;
mod preproc;
mod python;
mod ruby;
mod rust;
mod tcl;
mod tsx;
mod typescript;
#[cfg(test)]
mod node_text_tests {
use super::node_text;
use crate::langs::RustParser;
use crate::traits::ParserTrait;
use std::path::PathBuf;
#[cfg(feature = "rust")]
#[test]
fn in_bounds_span_returns_text() {
let src = "fn x() {}";
let parser = RustParser::new(src.as_bytes().to_vec(), &PathBuf::from("t.rs"), None);
let root = parser.root();
assert_eq!(node_text(parser.code(), &root), Some(src));
}
#[cfg(feature = "rust")]
#[test]
fn out_of_bounds_span_returns_none_not_panic() {
let src = "fn x() {}";
let parser = RustParser::new(src.as_bytes().to_vec(), &PathBuf::from("t.rs"), None);
let root = parser.root();
assert!(root.end_byte() > 2);
let truncated = &src.as_bytes()[..2];
assert_eq!(node_text(truncated, &root), None);
}
#[cfg(feature = "rust")]
#[test]
fn non_utf8_span_returns_none() {
let mut src = b"fn ".to_vec();
src.extend_from_slice(&[0xF0, 0x9F]);
src.extend_from_slice(b"() {}");
let parser = RustParser::new(src.clone(), &PathBuf::from("t.rs"), None);
let root = parser.root();
assert_eq!(root.end_byte(), src.len());
assert_eq!(node_text(parser.code(), &root), None);
}
}
#[cfg(test)]
mod ancestor_tests {
use super::Getter;
use crate::node::{Ancestors, Node};
use crate::test_support::for_each_node_with_chain;
use crate::traits::LanguageInfo;
#[cfg(any(
feature = "elixir",
feature = "javascript",
feature = "mozjs",
feature = "typescript",
))]
fn assert_name_parity<L: LanguageInfo + Getter>(
label: &str,
code: &[u8],
expect_named: &[&str],
) {
let mut seen: Vec<&str> = Vec::new();
let visited = for_each_node_with_chain::<L>(code, |node: &Node<'_>, chain| {
let known = L::get_func_space_name(node, code, Ancestors::known(chain));
let climbing = L::get_func_space_name(node, code, Ancestors::unknown());
assert_eq!(
known,
climbing,
"{label}: name of {} at row {} disagrees",
node.kind(),
node.start_row()
);
if let Some(name) = known
&& expect_named.contains(&name)
&& !seen.contains(&name)
{
seen.push(name);
}
});
assert!(visited > 20, "{label}: fixture is too small to prove much");
for name in expect_named {
assert!(
seen.contains(name),
"{label}: no node resolved to {name:?}, so the fixture no longer \
exercises the ancestor-derived naming it was added for"
);
}
}
#[cfg(all(
feature = "elixir",
feature = "javascript",
feature = "mozjs",
feature = "typescript",
))]
#[test]
fn func_space_name_agrees_between_known_and_climbing() {
let js_source =
b"var outer = function () { return 1; };\nvar o = { keyed: function () { return 2; } };\n";
assert_name_parity::<crate::langs::JavascriptCode>(
"javascript",
js_source,
&["outer", "keyed"],
);
assert_name_parity::<crate::langs::MozjsCode>("mozjs", js_source, &["outer", "keyed"]);
assert_name_parity::<crate::langs::TypescriptCode>(
"typescript",
js_source,
&["outer", "keyed"],
);
assert_name_parity::<crate::langs::TsxCode>("tsx", js_source, &["outer", "keyed"]);
assert_name_parity::<crate::langs::ElixirCode>(
"elixir",
b"defmodule Foo do\n defmacro multi do\n quote do\n def a, do: 1\n end\n end\nend\n",
&["Foo", "multi"],
);
}
}
#[cfg(test)]
#[cfg(any(feature = "tcl", feature = "irules"))]
mod braced_slot_tests {
use super::{BracedWordKinds, Getter};
#[cfg(feature = "tcl")]
use crate::Tcl;
use crate::node::{Ancestors, Node};
use crate::test_support::for_each_node_with_chain;
use crate::traits::LanguageInfo;
fn assert_no_shallow_value_slot<L: LanguageInfo + Getter>(
label: &str,
code: &[u8],
kinds: &BracedWordKinds,
) {
let (mut roots, mut children) = (0, 0);
for_each_node_with_chain::<L>(code, |node: &Node<'_>, chain| {
if chain.len() > 1 {
return;
}
for ancestors in [Ancestors::known(chain), Ancestors::unknown()] {
assert!(
!L::is_braced_literal_slot(node, code, ancestors, kinds),
"{label}: a {} at depth {} filled a value slot",
node.kind(),
chain.len()
);
}
if chain.is_empty() {
roots += 1;
} else {
children += 1;
}
});
assert_eq!(roots, 1, "{label}: exactly one root");
assert!(
children > 1,
"{label}: the fixture must put several nodes directly under the \
root, or the depth-1 exit goes unexercised"
);
}
#[test]
#[cfg(feature = "tcl")]
fn tcl_no_node_above_an_argument_fills_a_value_slot() {
assert_no_shallow_value_slot::<crate::langs::TclCode>(
"tcl",
b"{a b}\nproc {my proc} {} {}\nnamespace export {c d}\n",
&crate::lang_helpers::tcl::BRACED_WORD_KINDS,
);
}
#[test]
#[cfg(feature = "irules")]
fn irules_no_node_above_an_argument_fills_a_value_slot() {
assert_no_shallow_value_slot::<crate::langs::IrulesCode>(
"irules",
b"{a b}\nproc {my proc} {} {}\nnamespace export {c d}\n",
&crate::lang_helpers::irules::BRACED_WORD_KINDS,
);
}
#[cfg(feature = "tcl")]
fn value_slot_texts(code: &[u8]) -> Vec<String> {
let tree = crate::node::Tree::new::<crate::langs::TclCode>(code);
tree.get_root()
.preorder()
.filter(|node| {
<crate::langs::TclCode as Getter>::is_braced_literal_slot(
node,
code,
Ancestors::unknown(),
&crate::lang_helpers::tcl::BRACED_WORD_KINDS,
)
})
.map(|node| node.utf8_text(code).unwrap_or_default().to_owned())
.collect()
}
#[test]
#[cfg(feature = "tcl")]
fn an_error_inside_a_script_argument_keeps_the_sibling_value_slots() {
let well_formed = value_slot_texts(b"trap {pat} {v} {puts j}\n");
assert_eq!(
well_formed,
["{pat}", "{v}"],
"the fixture must spell two value slots before an error is added"
);
assert_eq!(
value_slot_texts(b"trap {pat} {v} {puts ]}\n"),
well_formed,
"an error inside the script argument moves no sibling's index"
);
}
#[test]
#[cfg(feature = "tcl")]
fn an_error_occupying_a_slot_withdraws_the_layout() {
assert_eq!(
value_slot_texts(b"on {code} {v} {puts j}\n"),
["{code}", "{v}"],
"the fixture must spell two value slots before the error \
takes one of their places"
);
assert!(
value_slot_texts(b"on {code} ] {puts j}\n").is_empty(),
"an ERROR token in the argument list makes every position \
unreadable"
);
}
#[test]
#[cfg(feature = "tcl")]
fn a_handler_clause_of_the_wrong_arity_withdraws_the_layout() {
assert_eq!(
value_slot_texts(b"try {puts a} trap {p} {v} {puts b}\n"),
["{p}", "{v}"],
"a three-argument `trap` clause spells two value slots"
);
assert!(
value_slot_texts(b"try {puts a} trap {p} {v} {puts b} finally {puts c}\n").is_empty(),
"a trailing `finally` joins the same generic `trap` command, \
so no position in its argument list is trustworthy"
);
}
#[test]
#[cfg(feature = "tcl")]
fn a_command_without_arguments_has_no_switch_arm_body() {
use crate::node::Tree;
let code = b"switch\nswitch $v { p {puts a} }\n";
let tree = Tree::new::<crate::langs::TclCode>(code);
let root = tree.get_root();
let bare = root.child(0).expect("the bare `switch` command");
let name = bare
.child_by_field_name("name")
.expect("even a bare command names itself");
assert!(
bare.child_by_field_name("arguments").is_none(),
"the fixture's first command must carry no argument list"
);
assert!(
!<crate::langs::TclCode as Getter>::is_switch_arm_body(&name, &bare),
"a command with no argument list has no body position"
);
let arm = find_arm_named_p(&root, code).expect("the arm command named `p`");
let body = arm
.child_by_field_name("arguments")
.and_then(|args| args.child(0))
.expect("the arm's body");
assert!(
<crate::langs::TclCode as Getter>::is_switch_arm_body(&body, &arm),
"the arm's sole argument is its body, or the assertion above is \
satisfied by a constant"
);
}
#[cfg(feature = "tcl")]
fn find_arm_named_p<'a>(node: &Node<'a>, code: &[u8]) -> Option<Node<'a>> {
let named_p = node.kind_id() == Tcl::Command as u16
&& node
.child_by_field_name("name")
.is_some_and(|name| name.utf8_text(code) == Some("p"));
if named_p {
return Some(*node);
}
node.children()
.find_map(|child| find_arm_named_p(&child, code))
}
fn assert_word_list_owners<L: LanguageInfo>(label: &str, code: &[u8], kinds: &BracedWordKinds) {
let mut seen = 0;
for_each_node_with_chain::<L>(code, |node: &Node<'_>, chain| {
if node.kind_id() != kinds.word_list {
return;
}
let owner = chain.last().expect("a word_list is never the root");
assert!(
[kinds.command, kinds.namespace].contains(&owner.kind_id()),
"{label}: a word_list owned by {}",
owner.kind()
);
seen += 1;
});
assert!(seen > 2, "{label}: fixture reached only {seen} word_lists");
}
const WORD_LIST_OWNER_FIXTURE: &[u8] = b"proc p {x} { puts $x }\n\
namespace eval ns { puts hi }\n\
namespace export {a b}\n\
if {$x} { puts a } else { puts b }\n\
while {$x} { puts a }\n\
foreach i {1 2} { puts $i }\n\
catch { puts a } msg\n\
eval {puts hi}\n\
switch $v { p {puts a} }\n\
lappend l {c d}\n";
#[test]
#[cfg(feature = "tcl")]
fn tcl_a_word_list_is_owned_only_by_a_command_or_a_namespace() {
assert_word_list_owners::<crate::langs::TclCode>(
"tcl",
WORD_LIST_OWNER_FIXTURE,
&crate::lang_helpers::tcl::BRACED_WORD_KINDS,
);
}
#[test]
#[cfg(feature = "irules")]
fn irules_a_word_list_is_owned_only_by_a_command_or_a_namespace() {
assert_word_list_owners::<crate::langs::IrulesCode>(
"irules",
WORD_LIST_OWNER_FIXTURE,
&crate::lang_helpers::irules::BRACED_WORD_KINDS,
);
}
}