use std::path::Path;
use tree_sitter::Language;
use crate::code::config_files;
#[derive(Clone, Copy)]
pub(in crate::code) struct LanguageSpec {
pub(in crate::code) id: &'static str,
pub(in crate::code) language: fn() -> Language,
pub(in crate::code) tags_query: &'static str,
}
const KOTLIN_TAGS_QUERY: &str = r#"
(package_header
(qualified_identifier) @name) @definition.module
(function_declaration
name: (identifier) @name) @definition.function
(class_declaration
name: (identifier) @name) @definition.class
(object_declaration
name: (identifier) @name) @definition.module
"#;
const SCALA_TAGS_QUERY: &str = r#"
(package_clause
name: (package_identifier) @name) @definition.module
(trait_definition
name: (identifier) @name) @definition.interface
(class_definition
name: (identifier) @name) @definition.class
(object_definition
name: (identifier) @name) @definition.module
(function_definition
name: (identifier) @name) @definition.function
(function_declaration
name: (identifier) @name) @definition.function
(call_expression
(identifier) @name) @reference.call
"#;
const BASH_TAGS_QUERY: &str = r#"
(function_definition
name: (word) @name) @definition.function
(command
name: (command_name) @name) @reference.call
"#;
const CONFIG_TAGS_QUERY: &str = "";
pub(in crate::code) fn language_id(path: &str) -> Option<&'static str> {
detect_language(path).map(|language| language.id)
}
pub(in crate::code) fn detect_language(path: &str) -> Option<LanguageSpec> {
let file_name = Path::new(path).file_name()?.to_str()?;
if matches!(
file_name,
".bash_profile" | ".bashrc" | ".profile" | "bashrc" | "bash_profile"
) {
return Some(bash());
}
if matches!(file_name, "Gemfile" | "Rakefile") {
return Some(ruby());
}
if dependency_only_manifest_file(file_name) {
return None;
}
if let Some(language) = config_files::detect(path) {
return Some(language);
}
let extension = Path::new(path).extension()?.to_str()?;
language_for_extension(extension)
}
fn dependency_only_manifest_file(file_name: &str) -> bool {
matches!(file_name, "package-lock.json")
}
pub(in crate::code) fn strip_supported_extension(path: &str) -> &str {
if dockerfile_prefix_variant(path) {
return path;
}
let Some(extension) = Path::new(path).extension().and_then(|value| value.to_str()) else {
return path;
};
if language_for_extension(extension)
.or_else(|| config_files::detect(path))
.is_none()
{
return path;
}
let extension_start = path.len().saturating_sub(extension.len() + 1);
&path[..extension_start]
}
fn dockerfile_prefix_variant(path: &str) -> bool {
let Some(file_name) = Path::new(path).file_name().and_then(|value| value.to_str()) else {
return false;
};
file_name.starts_with("Dockerfile.") || file_name.starts_with("Containerfile.")
}
pub(in crate::code) fn doc_comment_text<'a>(
trimmed: &'a str,
language_id: &str,
) -> Option<&'a str> {
match language_id {
"rust" => strip_comment_prefix(trimmed, &["///", "//!"]),
"python" | "ruby" | "bash" => strip_comment_prefix(trimmed, &["#"]),
"php" => strip_comment_prefix(trimmed, &["///", "//", "#"]),
"c" | "cpp" | "csharp" | "go" | "java" | "javascript" | "jsx" | "kotlin" | "scala"
| "swift" | "typescript" | "tsx" => strip_comment_prefix(trimmed, &["///", "//!", "//"]),
_ => config_files::doc_comment_text(trimmed, language_id),
}
}
pub(in crate::code) fn python_builtin_type_reference(name: &str) -> bool {
matches!(
name,
"Annotated"
| "Any"
| "AsyncIterable"
| "AsyncIterator"
| "Awaitable"
| "BinaryIO"
| "Callable"
| "ClassVar"
| "Concatenate"
| "Coroutine"
| "DefaultDict"
| "Deque"
| "Dict"
| "Final"
| "FrozenSet"
| "Generator"
| "Generic"
| "IO"
| "Iterable"
| "Iterator"
| "List"
| "Literal"
| "Mapping"
| "Match"
| "MutableMapping"
| "Never"
| "NewType"
| "NoReturn"
| "None"
| "NotRequired"
| "Optional"
| "ParamSpec"
| "Pattern"
| "Protocol"
| "Required"
| "Self"
| "Sequence"
| "Set"
| "TextIO"
| "True"
| "Tuple"
| "Type"
| "TypeAlias"
| "TypeVar"
| "TypedDict"
| "Union"
)
}
pub(in crate::code) fn typescript_builtin_type_reference(name: &str) -> bool {
matches!(
name,
"Array"
| "AsyncGenerator"
| "AsyncIterable"
| "AsyncIterator"
| "Awaited"
| "BigInt"
| "Boolean"
| "Capitalize"
| "ConstructorParameters"
| "Date"
| "Error"
| "Exclude"
| "Extract"
| "Function"
| "InstanceType"
| "Iterable"
| "Iterator"
| "Lowercase"
| "Map"
| "NonNullable"
| "Object"
| "Omit"
| "OmitThisParameter"
| "Parameters"
| "Partial"
| "Pick"
| "Promise"
| "PromiseLike"
| "Readonly"
| "ReadonlyArray"
| "ReadonlyMap"
| "ReadonlySet"
| "Record"
| "RegExp"
| "Required"
| "ReturnType"
| "Set"
| "String"
| "Symbol"
| "ThisParameterType"
| "ThisType"
| "Uncapitalize"
| "Uppercase"
| "WeakMap"
| "WeakSet"
)
}
fn strip_comment_prefix<'a>(trimmed: &'a str, prefixes: &[&str]) -> Option<&'a str> {
prefixes
.iter()
.find_map(|prefix| trimmed.strip_prefix(prefix).map(str::trim))
}
fn language_for_extension(extension: &str) -> Option<LanguageSpec> {
match extension.to_ascii_lowercase().as_str() {
"rs" => Some(LanguageSpec {
id: "rust",
language: || tree_sitter_rust::LANGUAGE.into(),
tags_query: tree_sitter_rust::TAGS_QUERY,
}),
"py" | "pyw" => Some(LanguageSpec {
id: "python",
language: || tree_sitter_python::LANGUAGE.into(),
tags_query: tree_sitter_python::TAGS_QUERY,
}),
"js" | "mjs" | "cjs" => Some(javascript()),
"json" => Some(LanguageSpec {
id: "json",
language: || tree_sitter_json::LANGUAGE.into(),
tags_query: CONFIG_TAGS_QUERY,
}),
"yaml" | "yml" => Some(LanguageSpec {
id: "yaml",
language: || tree_sitter_yaml::LANGUAGE.into(),
tags_query: CONFIG_TAGS_QUERY,
}),
"properties" => Some(LanguageSpec {
id: "properties",
language: || tree_sitter_properties::LANGUAGE.into(),
tags_query: CONFIG_TAGS_QUERY,
}),
"jsx" => Some(LanguageSpec {
id: "jsx",
language: || tree_sitter_javascript::LANGUAGE.into(),
tags_query: tree_sitter_javascript::TAGS_QUERY,
}),
"html" | "htm" => Some(LanguageSpec {
id: "html",
language: || tree_sitter_html::LANGUAGE.into(),
tags_query: CONFIG_TAGS_QUERY,
}),
"ts" | "mts" | "cts" => Some(LanguageSpec {
id: "typescript",
language: || tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
tags_query: tree_sitter_typescript::TAGS_QUERY,
}),
"tsx" => Some(LanguageSpec {
id: "tsx",
language: || tree_sitter_typescript::LANGUAGE_TSX.into(),
tags_query: tree_sitter_typescript::TAGS_QUERY,
}),
"vue" => Some(LanguageSpec {
id: "vue",
language: || tree_sitter_html::LANGUAGE.into(),
tags_query: CONFIG_TAGS_QUERY,
}),
"go" => Some(LanguageSpec {
id: "go",
language: || tree_sitter_go::LANGUAGE.into(),
tags_query: tree_sitter_go::TAGS_QUERY,
}),
"java" => Some(LanguageSpec {
id: "java",
language: || tree_sitter_java::LANGUAGE.into(),
tags_query: tree_sitter_java::TAGS_QUERY,
}),
"kt" | "kts" => Some(LanguageSpec {
id: "kotlin",
language: || tree_sitter_kotlin_ng::LANGUAGE.into(),
tags_query: KOTLIN_TAGS_QUERY,
}),
"scala" | "sc" => Some(LanguageSpec {
id: "scala",
language: || tree_sitter_scala::LANGUAGE.into(),
tags_query: SCALA_TAGS_QUERY,
}),
"c" => Some(LanguageSpec {
id: "c",
language: || tree_sitter_c::LANGUAGE.into(),
tags_query: tree_sitter_c::TAGS_QUERY,
}),
"h" => Some(LanguageSpec {
id: "c",
language: || tree_sitter_c::LANGUAGE.into(),
tags_query: tree_sitter_c::TAGS_QUERY,
}),
"cc" | "cpp" | "cxx" | "c++" | "hh" | "hpp" | "hxx" | "h++" => Some(LanguageSpec {
id: "cpp",
language: || tree_sitter_cpp::LANGUAGE.into(),
tags_query: tree_sitter_cpp::TAGS_QUERY,
}),
"cs" => Some(LanguageSpec {
id: "csharp",
language: || tree_sitter_c_sharp::LANGUAGE.into(),
tags_query: tree_sitter_c_sharp::TAGS_QUERY,
}),
"rb" => Some(ruby()),
"php" | "phtml" => Some(LanguageSpec {
id: "php",
language: || tree_sitter_php::LANGUAGE_PHP.into(),
tags_query: tree_sitter_php::TAGS_QUERY,
}),
"swift" => Some(LanguageSpec {
id: "swift",
language: || tree_sitter_swift::LANGUAGE.into(),
tags_query: tree_sitter_swift::TAGS_QUERY,
}),
"sql" => Some(LanguageSpec {
id: "sql",
language: || tree_sitter_sequel::LANGUAGE.into(),
tags_query: CONFIG_TAGS_QUERY,
}),
"sh" | "bash" | "bats" => Some(bash()),
_ => None,
}
}
fn javascript() -> LanguageSpec {
LanguageSpec {
id: "javascript",
language: || tree_sitter_javascript::LANGUAGE.into(),
tags_query: tree_sitter_javascript::TAGS_QUERY,
}
}
fn ruby() -> LanguageSpec {
LanguageSpec {
id: "ruby",
language: || tree_sitter_ruby::LANGUAGE.into(),
tags_query: tree_sitter_ruby::TAGS_QUERY,
}
}
fn bash() -> LanguageSpec {
LanguageSpec {
id: "bash",
language: || tree_sitter_bash::LANGUAGE.into(),
tags_query: BASH_TAGS_QUERY,
}
}
#[cfg(test)]
#[path = "mod_tests.rs"]
mod tests;