feat(pi-natives): added language module with 39 languages and tree-sitter bindings
- Added language module with 39 supported languages and tree-sitter parser bindings for ast-grep integration. - Exported dedupeParseErrors utility function to remove duplicate parse error messages in tool output. - Fixed duplicate parse error messages when multiple patterns fail on the same file. - Replaced language alias lookup with O(1) phf::Map and added 30+ new language aliases (golang, julia, toml, zig, etc.). - Migrated from ast-grep-language to direct tree-sitter language dependencies for improved language support.
This commit is contained in:
Generated
+158
-38
@@ -150,43 +150,6 @@ dependencies = [
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ast-grep-language"
|
||||
version = "0.39.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24b571f7f8cde8bd77ea48f63a81094b7ce0695da50d0dc956cc45f1e26533ce"
|
||||
dependencies = [
|
||||
"ast-grep-core",
|
||||
"ignore",
|
||||
"serde",
|
||||
"tree-sitter",
|
||||
"tree-sitter-bash",
|
||||
"tree-sitter-c",
|
||||
"tree-sitter-c-sharp",
|
||||
"tree-sitter-cpp",
|
||||
"tree-sitter-css",
|
||||
"tree-sitter-elixir",
|
||||
"tree-sitter-go",
|
||||
"tree-sitter-haskell",
|
||||
"tree-sitter-hcl",
|
||||
"tree-sitter-html",
|
||||
"tree-sitter-java",
|
||||
"tree-sitter-javascript",
|
||||
"tree-sitter-json",
|
||||
"tree-sitter-kotlin-sg",
|
||||
"tree-sitter-lua",
|
||||
"tree-sitter-nix",
|
||||
"tree-sitter-php",
|
||||
"tree-sitter-python",
|
||||
"tree-sitter-ruby",
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-scala",
|
||||
"tree-sitter-solidity",
|
||||
"tree-sitter-swift",
|
||||
"tree-sitter-typescript",
|
||||
"tree-sitter-yaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "astral-tl"
|
||||
version = "0.7.11"
|
||||
@@ -2200,7 +2163,6 @@ version = "13.7.5"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"ast-grep-core",
|
||||
"ast-grep-language",
|
||||
"brush-builtins",
|
||||
"brush-core",
|
||||
"bstr",
|
||||
@@ -2230,6 +2192,44 @@ dependencies = [
|
||||
"sysinfo",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tree-sitter",
|
||||
"tree-sitter-bash",
|
||||
"tree-sitter-c",
|
||||
"tree-sitter-c-sharp",
|
||||
"tree-sitter-cpp",
|
||||
"tree-sitter-css",
|
||||
"tree-sitter-diff",
|
||||
"tree-sitter-elixir",
|
||||
"tree-sitter-go",
|
||||
"tree-sitter-haskell",
|
||||
"tree-sitter-hcl",
|
||||
"tree-sitter-html",
|
||||
"tree-sitter-java",
|
||||
"tree-sitter-javascript",
|
||||
"tree-sitter-json",
|
||||
"tree-sitter-julia",
|
||||
"tree-sitter-kotlin-sg",
|
||||
"tree-sitter-lua",
|
||||
"tree-sitter-make",
|
||||
"tree-sitter-md",
|
||||
"tree-sitter-nix",
|
||||
"tree-sitter-objc",
|
||||
"tree-sitter-odin",
|
||||
"tree-sitter-php",
|
||||
"tree-sitter-python",
|
||||
"tree-sitter-regex",
|
||||
"tree-sitter-ruby",
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-scala",
|
||||
"tree-sitter-solidity",
|
||||
"tree-sitter-starlark",
|
||||
"tree-sitter-swift",
|
||||
"tree-sitter-toml-ng",
|
||||
"tree-sitter-typescript",
|
||||
"tree-sitter-verilog",
|
||||
"tree-sitter-xml",
|
||||
"tree-sitter-yaml",
|
||||
"tree-sitter-zig",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
"windows-sys 0.61.2",
|
||||
@@ -3152,6 +3152,16 @@ dependencies = [
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-diff"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfe1e5ca280a65dfe5ba4205c1bcc84edf486464fed315db53dee6da9a335889"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-elixir"
|
||||
version = "0.3.5"
|
||||
@@ -3232,6 +3242,16 @@ dependencies = [
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-julia"
|
||||
version = "0.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4144731a178812ee867619b1e98b3b91e54c1652304b26e5ebe3175b701de323"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-kotlin-sg"
|
||||
version = "0.4.0"
|
||||
@@ -3258,6 +3278,26 @@ dependencies = [
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-make"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5998dc7cbcbdab19fae8aefef982bf2d6544513d8d2e69cc44aec4c63810104"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-md"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2efd398be546456c814598ee56c0f51769a77241511b4a58077815d120afa882"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-nix"
|
||||
version = "0.3.0"
|
||||
@@ -3268,6 +3308,26 @@ dependencies = [
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-objc"
|
||||
version = "3.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ca8bb556423fc176f0535e79d525f783a6684d3c9da81bf9d905303c129e1d2"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-odin"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24db210fe9ba2237c71c5030d7b146c7025420ba72dd8013d13cd822c3a8d77a"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-php"
|
||||
version = "0.24.2"
|
||||
@@ -3288,6 +3348,16 @@ dependencies = [
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-regex"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd8a59be9f0ac131fd8f062eaaba14882b2fa5a6a7882a20134cb1d60df2e625"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-ruby"
|
||||
version = "0.23.1"
|
||||
@@ -3328,6 +3398,16 @@ dependencies = [
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-starlark"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8934f282d085cc4b9ee28aa688aa3fbe8aa3766201c2a6252f411d45b4c3a721"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-swift"
|
||||
version = "0.7.1"
|
||||
@@ -3338,6 +3418,16 @@ dependencies = [
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-toml-ng"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9adc2c898ae49730e857d75be403da3f92bb81d8e37a2f918a08dd10de5ebb1"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-typescript"
|
||||
version = "0.23.2"
|
||||
@@ -3348,6 +3438,26 @@ dependencies = [
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-verilog"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4e7e0360395852f1f6ff5b7b82c72dc6557d181073188df1d60ec469ea69c66"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-xml"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e670041f591d994f54d597ddcd8f4ebc930e282c4c76a42268743b71f0c8b6b3"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-yaml"
|
||||
version = "0.7.2"
|
||||
@@ -3358,6 +3468,16 @@ dependencies = [
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-zig"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab11fc124851b0db4dd5e55983bbd9631192e93238389dcd44521715e5d53e28"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree_magic_mini"
|
||||
version = "3.2.2"
|
||||
|
||||
@@ -31,8 +31,45 @@ grep-matcher = "0.1"
|
||||
globset = "0.4"
|
||||
ignore = "0.4"
|
||||
rayon = "1.10"
|
||||
ast-grep-core = "0.39"
|
||||
ast-grep-language = "0.39"
|
||||
ast-grep-core = { version = "0.39", default-features = false, features = ["tree-sitter"] }
|
||||
tree-sitter = "0.25"
|
||||
tree-sitter-bash = "0.25"
|
||||
tree-sitter-c = "0.24"
|
||||
tree-sitter-c-sharp = "0.23"
|
||||
tree-sitter-cpp = "0.23"
|
||||
tree-sitter-css = "0.25"
|
||||
tree-sitter-diff = "0.1"
|
||||
tree-sitter-elixir = "0.3"
|
||||
tree-sitter-go = "0.25"
|
||||
tree-sitter-haskell = "0.23"
|
||||
tree-sitter-hcl = "1.1"
|
||||
tree-sitter-html = "0.23"
|
||||
tree-sitter-java = "0.23"
|
||||
tree-sitter-javascript = "0.25"
|
||||
tree-sitter-json = "0.23"
|
||||
tree-sitter-julia = "0.23"
|
||||
tree-sitter-kotlin = { version = "0.4", package = "tree-sitter-kotlin-sg" }
|
||||
tree-sitter-lua = "0.2"
|
||||
tree-sitter-make = "1.1"
|
||||
tree-sitter-md = "0.5"
|
||||
tree-sitter-nix = "0.3"
|
||||
tree-sitter-objc = "3.0"
|
||||
tree-sitter-odin = "1.3"
|
||||
tree-sitter-php = "0.24"
|
||||
tree-sitter-python = "0.25"
|
||||
tree-sitter-regex = "0.25"
|
||||
tree-sitter-ruby = "0.23"
|
||||
tree-sitter-rust = "0.24"
|
||||
tree-sitter-scala = "0.24"
|
||||
tree-sitter-solidity = "1.2"
|
||||
tree-sitter-starlark = "1.3"
|
||||
tree-sitter-swift = "0.7"
|
||||
tree-sitter-toml-ng = "0.7"
|
||||
tree-sitter-typescript = "0.23"
|
||||
tree-sitter-verilog = "1.0"
|
||||
tree-sitter-xml = "0.7"
|
||||
tree-sitter-yaml = "0.7"
|
||||
tree-sitter-zig = "1.1"
|
||||
inferno = { version = "0.12", default-features = false }
|
||||
image = { version = "0.25", default-features = false, features = [
|
||||
"png",
|
||||
|
||||
+105
-158
@@ -6,16 +6,12 @@ use std::{
|
||||
};
|
||||
|
||||
use ast_grep_core::{
|
||||
Language, MatchStrictness,
|
||||
matcher::Pattern,
|
||||
source::{Doc, Edit},
|
||||
tree_sitter::LanguageExt,
|
||||
Language, MatchStrictness, matcher::Pattern, source::Edit, tree_sitter::LanguageExt,
|
||||
};
|
||||
use ast_grep_language::SupportLang;
|
||||
use napi::bindgen_prelude::*;
|
||||
use napi_derive::napi;
|
||||
|
||||
use crate::{fs_cache, glob_util, task};
|
||||
use crate::{fs_cache, glob_util, language::SupportLang, task};
|
||||
|
||||
const DEFAULT_FIND_LIMIT: u32 = 50;
|
||||
|
||||
@@ -152,111 +148,112 @@ fn to_u32(value: usize) -> u32 {
|
||||
value.min(u32::MAX as usize) as u32
|
||||
}
|
||||
|
||||
const fn supported_lang_aliases() -> &'static [&'static str] {
|
||||
&[
|
||||
"bash",
|
||||
"sh",
|
||||
"c",
|
||||
"cpp",
|
||||
"c++",
|
||||
"cc",
|
||||
"cxx",
|
||||
"csharp",
|
||||
"c#",
|
||||
"cs",
|
||||
"css",
|
||||
"elixir",
|
||||
"ex",
|
||||
"go",
|
||||
"haskell",
|
||||
"hs",
|
||||
"hcl",
|
||||
"tf",
|
||||
"html",
|
||||
"java",
|
||||
"javascript",
|
||||
"js",
|
||||
"jsx",
|
||||
"json",
|
||||
"kotlin",
|
||||
"kt",
|
||||
"lua",
|
||||
"nix",
|
||||
"php",
|
||||
"python",
|
||||
"py",
|
||||
"ruby",
|
||||
"rb",
|
||||
"rust",
|
||||
"rs",
|
||||
"scala",
|
||||
"solidity",
|
||||
"sol",
|
||||
"swift",
|
||||
"tsx",
|
||||
"typescript",
|
||||
"ts",
|
||||
"yaml",
|
||||
"yml",
|
||||
]
|
||||
/// Single source of truth: every recognised alias (lowercased) → `SupportLang`.
|
||||
/// `resolve_supported_lang` does a lookup here; error messages list the keys.
|
||||
static LANG_ALIASES: phf::Map<&'static str, SupportLang> = phf::phf_map! {
|
||||
"bash" => SupportLang::Bash,
|
||||
"sh" => SupportLang::Bash,
|
||||
"c" => SupportLang::C,
|
||||
"cpp" => SupportLang::Cpp,
|
||||
"c++" => SupportLang::Cpp,
|
||||
"cc" => SupportLang::Cpp,
|
||||
"cxx" => SupportLang::Cpp,
|
||||
"csharp" => SupportLang::CSharp,
|
||||
"c#" => SupportLang::CSharp,
|
||||
"cs" => SupportLang::CSharp,
|
||||
"css" => SupportLang::Css,
|
||||
"diff" => SupportLang::Diff,
|
||||
"patch" => SupportLang::Diff,
|
||||
"elixir" => SupportLang::Elixir,
|
||||
"ex" => SupportLang::Elixir,
|
||||
"go" => SupportLang::Go,
|
||||
"golang" => SupportLang::Go,
|
||||
"haskell" => SupportLang::Haskell,
|
||||
"hs" => SupportLang::Haskell,
|
||||
"hcl" => SupportLang::Hcl,
|
||||
"tf" => SupportLang::Hcl,
|
||||
"tfvars" => SupportLang::Hcl,
|
||||
"terraform" => SupportLang::Hcl,
|
||||
"html" => SupportLang::Html,
|
||||
"htm" => SupportLang::Html,
|
||||
"java" => SupportLang::Java,
|
||||
"javascript" => SupportLang::JavaScript,
|
||||
"js" => SupportLang::JavaScript,
|
||||
"jsx" => SupportLang::JavaScript,
|
||||
"mjs" => SupportLang::JavaScript,
|
||||
"cjs" => SupportLang::JavaScript,
|
||||
"json" => SupportLang::Json,
|
||||
"julia" => SupportLang::Julia,
|
||||
"jl" => SupportLang::Julia,
|
||||
"kotlin" => SupportLang::Kotlin,
|
||||
"kt" => SupportLang::Kotlin,
|
||||
"lua" => SupportLang::Lua,
|
||||
"make" => SupportLang::Make,
|
||||
"makefile" => SupportLang::Make,
|
||||
"markdown" => SupportLang::Markdown,
|
||||
"md" => SupportLang::Markdown,
|
||||
"mdx" => SupportLang::Markdown,
|
||||
"nix" => SupportLang::Nix,
|
||||
"objc" => SupportLang::ObjC,
|
||||
"objective-c" => SupportLang::ObjC,
|
||||
"odin" => SupportLang::Odin,
|
||||
"php" => SupportLang::Php,
|
||||
"python" => SupportLang::Python,
|
||||
"py" => SupportLang::Python,
|
||||
"regex" => SupportLang::Regex,
|
||||
"ruby" => SupportLang::Ruby,
|
||||
"rb" => SupportLang::Ruby,
|
||||
"rust" => SupportLang::Rust,
|
||||
"rs" => SupportLang::Rust,
|
||||
"scala" => SupportLang::Scala,
|
||||
"solidity" => SupportLang::Solidity,
|
||||
"sol" => SupportLang::Solidity,
|
||||
"starlark" => SupportLang::Starlark,
|
||||
"star" => SupportLang::Starlark,
|
||||
"swift" => SupportLang::Swift,
|
||||
"toml" => SupportLang::Toml,
|
||||
"tsx" => SupportLang::Tsx,
|
||||
"typescript" => SupportLang::TypeScript,
|
||||
"ts" => SupportLang::TypeScript,
|
||||
"mts" => SupportLang::TypeScript,
|
||||
"cts" => SupportLang::TypeScript,
|
||||
"verilog" => SupportLang::Verilog,
|
||||
"systemverilog" => SupportLang::Verilog,
|
||||
"sv" => SupportLang::Verilog,
|
||||
"xml" => SupportLang::Xml,
|
||||
"xsl" => SupportLang::Xml,
|
||||
"svg" => SupportLang::Xml,
|
||||
"yaml" => SupportLang::Yaml,
|
||||
"yml" => SupportLang::Yaml,
|
||||
"zig" => SupportLang::Zig,
|
||||
};
|
||||
|
||||
fn supported_lang_list() -> String {
|
||||
let mut keys: Vec<&str> = LANG_ALIASES.keys().copied().collect();
|
||||
keys.sort_unstable();
|
||||
keys.join(", ")
|
||||
}
|
||||
|
||||
fn resolve_supported_lang(value: &str) -> Result<SupportLang> {
|
||||
match value.to_ascii_lowercase().as_str() {
|
||||
"bash" | "sh" => Ok(SupportLang::Bash),
|
||||
"c" => Ok(SupportLang::C),
|
||||
"cpp" | "c++" | "cc" | "cxx" => Ok(SupportLang::Cpp),
|
||||
"csharp" | "c#" | "cs" => Ok(SupportLang::CSharp),
|
||||
"css" => Ok(SupportLang::Css),
|
||||
"elixir" | "ex" => Ok(SupportLang::Elixir),
|
||||
"go" => Ok(SupportLang::Go),
|
||||
"haskell" | "hs" => Ok(SupportLang::Haskell),
|
||||
"hcl" | "tf" => Ok(SupportLang::Hcl),
|
||||
"html" => Ok(SupportLang::Html),
|
||||
"java" => Ok(SupportLang::Java),
|
||||
"javascript" | "js" | "jsx" => Ok(SupportLang::JavaScript),
|
||||
"json" => Ok(SupportLang::Json),
|
||||
"kotlin" | "kt" => Ok(SupportLang::Kotlin),
|
||||
"lua" => Ok(SupportLang::Lua),
|
||||
"nix" => Ok(SupportLang::Nix),
|
||||
"php" => Ok(SupportLang::Php),
|
||||
"python" | "py" => Ok(SupportLang::Python),
|
||||
"ruby" | "rb" => Ok(SupportLang::Ruby),
|
||||
"rust" | "rs" => Ok(SupportLang::Rust),
|
||||
"scala" => Ok(SupportLang::Scala),
|
||||
"solidity" | "sol" => Ok(SupportLang::Solidity),
|
||||
"swift" => Ok(SupportLang::Swift),
|
||||
"tsx" => Ok(SupportLang::Tsx),
|
||||
"typescript" | "ts" => Ok(SupportLang::TypeScript),
|
||||
"yaml" | "yml" => Ok(SupportLang::Yaml),
|
||||
_ => Err(Error::from_reason(format!(
|
||||
let lower = value.to_ascii_lowercase();
|
||||
LANG_ALIASES.get(lower.as_str()).copied().ok_or_else(|| {
|
||||
Error::from_reason(format!(
|
||||
"Unsupported language '{value}'. Supported: {}",
|
||||
supported_lang_aliases().join(", ")
|
||||
))),
|
||||
}
|
||||
supported_lang_list()
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_language(lang: Option<&str>, file_path: &Path) -> Result<SupportLang> {
|
||||
if let Some(lang) = lang.map(str::trim).filter(|lang| !lang.is_empty()) {
|
||||
return resolve_supported_lang(lang);
|
||||
}
|
||||
let Some(guessed) = SupportLang::from_path(file_path) else {
|
||||
return Err(Error::from_reason(format!(
|
||||
SupportLang::from_path(file_path).ok_or_else(|| {
|
||||
Error::from_reason(format!(
|
||||
"Unable to infer language from file extension: {}. Specify `lang` explicitly.",
|
||||
file_path.display()
|
||||
)));
|
||||
};
|
||||
// Accept any language that ast-grep can infer from the extension,
|
||||
// but only if we also support it in resolve_supported_lang.
|
||||
let name = canonical_lang_name(guessed);
|
||||
if name == "unknown" {
|
||||
return Err(Error::from_reason(format!(
|
||||
"Unsupported inferred language for {}. Supported: {}",
|
||||
file_path.display(),
|
||||
supported_lang_aliases().join(", ")
|
||||
)));
|
||||
}
|
||||
Ok(guessed)
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns true if the file's extension resolves to a supported language.
|
||||
@@ -270,37 +267,6 @@ fn is_supported_file(file_path: &Path, explicit_lang: Option<&str>) -> bool {
|
||||
resolve_language(None, file_path).is_ok()
|
||||
}
|
||||
|
||||
const fn canonical_lang_name(lang: SupportLang) -> &'static str {
|
||||
match lang {
|
||||
SupportLang::Bash => "bash",
|
||||
SupportLang::C => "c",
|
||||
SupportLang::Cpp => "cpp",
|
||||
SupportLang::CSharp => "csharp",
|
||||
SupportLang::Css => "css",
|
||||
SupportLang::Elixir => "elixir",
|
||||
SupportLang::Go => "go",
|
||||
SupportLang::Haskell => "haskell",
|
||||
SupportLang::Hcl => "hcl",
|
||||
SupportLang::Html => "html",
|
||||
SupportLang::Java => "java",
|
||||
SupportLang::JavaScript => "javascript",
|
||||
SupportLang::Json => "json",
|
||||
SupportLang::Kotlin => "kotlin",
|
||||
SupportLang::Lua => "lua",
|
||||
SupportLang::Nix => "nix",
|
||||
SupportLang::Php => "php",
|
||||
SupportLang::Python => "python",
|
||||
SupportLang::Ruby => "ruby",
|
||||
SupportLang::Rust => "rust",
|
||||
SupportLang::Scala => "scala",
|
||||
SupportLang::Solidity => "solidity",
|
||||
SupportLang::Swift => "swift",
|
||||
SupportLang::Tsx => "tsx",
|
||||
SupportLang::TypeScript => "typescript",
|
||||
SupportLang::Yaml => "yaml",
|
||||
}
|
||||
}
|
||||
|
||||
fn infer_single_replace_lang(
|
||||
candidates: &[FileCandidate],
|
||||
ct: &task::CancelToken,
|
||||
@@ -311,7 +277,7 @@ fn infer_single_replace_lang(
|
||||
ct.heartbeat()?;
|
||||
match resolve_language(None, &candidate.absolute_path) {
|
||||
Ok(language) => {
|
||||
inferred.insert(canonical_lang_name(language).to_string());
|
||||
inferred.insert(language.canonical_name().to_string());
|
||||
},
|
||||
Err(err) => unresolved.push(format!("{}: {}", candidate.display_path, err)),
|
||||
}
|
||||
@@ -450,12 +416,6 @@ fn compile_pattern(
|
||||
Ok(compiled)
|
||||
}
|
||||
|
||||
fn has_syntax_error<D: Doc>(ast: &ast_grep_core::AstGrep<D>) -> bool {
|
||||
ast.root()
|
||||
.dfs()
|
||||
.any(|node| node.is_error() || node.is_missing())
|
||||
}
|
||||
|
||||
fn apply_edits(content: &str, edits: &[Edit<String>]) -> Result<String> {
|
||||
let mut sorted: Vec<&Edit<String>> = edits.iter().collect();
|
||||
sorted.sort_by_key(|edit| edit.position);
|
||||
@@ -550,7 +510,7 @@ fn resolve_candidates_for_find(
|
||||
ct.heartbeat()?;
|
||||
match resolve_language(lang, &candidate.absolute_path) {
|
||||
Ok(language) => {
|
||||
let key = canonical_lang_name(language).to_string();
|
||||
let key = language.canonical_name().to_string();
|
||||
languages.entry(key).or_insert(language);
|
||||
resolved.push(ResolvedCandidate {
|
||||
candidate,
|
||||
@@ -662,7 +622,7 @@ pub fn ast_grep(options: AstFindOptions<'_>) -> task::Async<AstFindResult> {
|
||||
let Some(language) = language else {
|
||||
continue;
|
||||
};
|
||||
let lang_key = canonical_lang_name(language);
|
||||
let lang_key = language.canonical_name();
|
||||
let source = match std::fs::read_to_string(&candidate.absolute_path) {
|
||||
Ok(source) => source,
|
||||
Err(err) => {
|
||||
@@ -691,14 +651,11 @@ pub fn ast_grep(options: AstFindOptions<'_>) -> task::Async<AstFindResult> {
|
||||
}
|
||||
|
||||
let ast = language.ast_grep(source);
|
||||
if has_syntax_error(&ast) {
|
||||
for (pattern_name, _) in &runnable_patterns {
|
||||
parse_errors.push(format!(
|
||||
"{pattern_name}: {}: parse error (syntax tree contains error nodes)",
|
||||
candidate.display_path
|
||||
));
|
||||
}
|
||||
continue;
|
||||
if ast.root().dfs().any(|node| node.is_error()) {
|
||||
parse_errors.push(format!(
|
||||
"{}: parse error (syntax tree contains error nodes)",
|
||||
candidate.display_path
|
||||
));
|
||||
}
|
||||
|
||||
for (_, pattern) in runnable_patterns {
|
||||
@@ -847,7 +804,7 @@ pub fn ast_edit(options: AstReplaceOptions<'_>) -> task::Async<AstReplaceResult>
|
||||
};
|
||||
|
||||
let ast = language.ast_grep(&source);
|
||||
if has_syntax_error(&ast) {
|
||||
if ast.root().dfs().any(|node| node.is_error()) {
|
||||
let parse_issue = format!(
|
||||
"{}: parse error (syntax tree contains error nodes)",
|
||||
candidate.display_path
|
||||
@@ -959,8 +916,6 @@ mod tests {
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use ast_grep_core::tree_sitter::LanguageExt;
|
||||
|
||||
use super::*;
|
||||
|
||||
struct TempTree {
|
||||
@@ -1060,14 +1015,6 @@ mod tests {
|
||||
assert!(resolve_supported_lang("brainfuck").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_syntax_errors_in_ast() {
|
||||
let ok = SupportLang::TypeScript.ast_grep("const value = 1;");
|
||||
assert!(!has_syntax_error(&ok));
|
||||
let bad = SupportLang::TypeScript.ast_grep("export function broken( { return 1; }");
|
||||
assert!(has_syntax_error(&bad));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn applies_non_overlapping_edits() {
|
||||
let source = "const answer = 41;";
|
||||
|
||||
@@ -0,0 +1,511 @@
|
||||
//! Vendored and extended language definitions for ast-grep integration.
|
||||
//!
|
||||
//! Originally derived from `ast-grep-language` v0.39.9, stripped of
|
||||
//! serde/ignore machinery, and extended with additional languages.
|
||||
|
||||
mod parsers;
|
||||
|
||||
use std::{borrow::Cow, collections::HashMap, fmt, path::Path};
|
||||
|
||||
use ast_grep_core::{
|
||||
Doc, Language, Node,
|
||||
matcher::{KindMatcher, Pattern, PatternBuilder, PatternError},
|
||||
meta_var::MetaVariable,
|
||||
tree_sitter::{LanguageExt, StrDoc, TSLanguage, TSRange},
|
||||
};
|
||||
|
||||
/// Implements a stub language (no expando / `pre_process_pattern` needed).
|
||||
/// Use when the language grammar accepts `$VAR` as valid identifiers.
|
||||
macro_rules! impl_lang {
|
||||
($lang:ident, $func:ident) => {
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct $lang;
|
||||
impl Language for $lang {
|
||||
fn kind_to_id(&self, kind: &str) -> u16 {
|
||||
self.get_ts_language().id_for_node_kind(kind, true)
|
||||
}
|
||||
|
||||
fn field_to_id(&self, field: &str) -> Option<u16> {
|
||||
self
|
||||
.get_ts_language()
|
||||
.field_id_for_name(field)
|
||||
.map(|f| f.get())
|
||||
}
|
||||
|
||||
fn build_pattern(&self, builder: &PatternBuilder) -> Result<Pattern, PatternError> {
|
||||
builder.build(|src| StrDoc::try_new(src, *self))
|
||||
}
|
||||
}
|
||||
impl LanguageExt for $lang {
|
||||
fn get_ts_language(&self) -> TSLanguage {
|
||||
parsers::$func().into()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn pre_process_pattern(expando: char, query: &str) -> Cow<'_, str> {
|
||||
let mut ret = Vec::with_capacity(query.len());
|
||||
let mut dollar_count = 0;
|
||||
for c in query.chars() {
|
||||
if c == '$' {
|
||||
dollar_count += 1;
|
||||
continue;
|
||||
}
|
||||
let need_replace = matches!(c, 'A'..='Z' | '_') || dollar_count == 3;
|
||||
let sigil = if need_replace { expando } else { '$' };
|
||||
ret.extend(std::iter::repeat_n(sigil, dollar_count));
|
||||
dollar_count = 0;
|
||||
ret.push(c);
|
||||
}
|
||||
let sigil = if dollar_count == 3 { expando } else { '$' };
|
||||
ret.extend(std::iter::repeat_n(sigil, dollar_count));
|
||||
Cow::Owned(ret.into_iter().collect())
|
||||
}
|
||||
|
||||
/// Implements a language with `expando_char` / `pre_process_pattern`.
|
||||
/// Use when the language does NOT accept `$` as a valid identifier character.
|
||||
macro_rules! impl_lang_expando {
|
||||
($lang:ident, $func:ident, $char:expr) => {
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct $lang;
|
||||
impl Language for $lang {
|
||||
fn kind_to_id(&self, kind: &str) -> u16 {
|
||||
self.get_ts_language().id_for_node_kind(kind, true)
|
||||
}
|
||||
|
||||
fn field_to_id(&self, field: &str) -> Option<u16> {
|
||||
self
|
||||
.get_ts_language()
|
||||
.field_id_for_name(field)
|
||||
.map(|f| f.get())
|
||||
}
|
||||
|
||||
fn expando_char(&self) -> char {
|
||||
$char
|
||||
}
|
||||
|
||||
fn pre_process_pattern<'q>(&self, query: &'q str) -> Cow<'q, str> {
|
||||
pre_process_pattern(self.expando_char(), query)
|
||||
}
|
||||
|
||||
fn build_pattern(&self, builder: &PatternBuilder) -> Result<Pattern, PatternError> {
|
||||
builder.build(|src| StrDoc::try_new(src, *self))
|
||||
}
|
||||
}
|
||||
impl LanguageExt for $lang {
|
||||
fn get_ts_language(&self) -> TSLanguage {
|
||||
parsers::$func().into()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ── Customized languages with expando_char ──────────────────────────────
|
||||
|
||||
impl_lang_expando!(C, language_c, '𐀀');
|
||||
impl_lang_expando!(Cpp, language_cpp, '𐀀');
|
||||
impl_lang_expando!(CSharp, language_c_sharp, 'µ');
|
||||
impl_lang_expando!(Css, language_css, '_');
|
||||
impl_lang_expando!(Elixir, language_elixir, 'µ');
|
||||
impl_lang_expando!(Go, language_go, 'µ');
|
||||
impl_lang_expando!(Haskell, language_haskell, 'µ');
|
||||
impl_lang_expando!(Hcl, language_hcl, 'µ');
|
||||
impl_lang_expando!(Kotlin, language_kotlin, 'µ');
|
||||
impl_lang_expando!(Nix, language_nix, '_');
|
||||
impl_lang_expando!(Php, language_php, 'µ');
|
||||
impl_lang_expando!(Python, language_python, 'µ');
|
||||
impl_lang_expando!(Ruby, language_ruby, 'µ');
|
||||
impl_lang_expando!(Rust, language_rust, 'µ');
|
||||
impl_lang_expando!(Swift, language_swift, 'µ');
|
||||
|
||||
// New expando languages
|
||||
impl_lang_expando!(Make, language_make, 'µ');
|
||||
impl_lang_expando!(ObjC, language_objc, '𐀀');
|
||||
impl_lang_expando!(Starlark, language_starlark, 'µ');
|
||||
impl_lang_expando!(Odin, language_odin, 'µ');
|
||||
impl_lang_expando!(Julia, language_julia, 'µ');
|
||||
impl_lang_expando!(Verilog, language_verilog, 'µ');
|
||||
impl_lang_expando!(Zig, language_zig, 'µ');
|
||||
|
||||
// ── Stub languages ($ accepted in grammar) ──────────────────────────────
|
||||
|
||||
impl_lang!(Bash, language_bash);
|
||||
impl_lang!(Java, language_java);
|
||||
impl_lang!(JavaScript, language_javascript);
|
||||
impl_lang!(Json, language_json);
|
||||
impl_lang!(Lua, language_lua);
|
||||
impl_lang!(Scala, language_scala);
|
||||
impl_lang!(Solidity, language_solidity);
|
||||
impl_lang!(Tsx, language_tsx);
|
||||
impl_lang!(TypeScript, language_typescript);
|
||||
impl_lang!(Yaml, language_yaml);
|
||||
|
||||
// New stub languages
|
||||
impl_lang!(Markdown, language_markdown);
|
||||
impl_lang!(Toml, language_toml);
|
||||
impl_lang!(Diff, language_diff);
|
||||
impl_lang!(Xml, language_xml);
|
||||
impl_lang!(Regex, language_regex);
|
||||
|
||||
// ── Html (custom implementation with injection support) ──────────────────
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Html;
|
||||
|
||||
impl Language for Html {
|
||||
fn expando_char(&self) -> char {
|
||||
'z'
|
||||
}
|
||||
|
||||
fn pre_process_pattern<'q>(&self, query: &'q str) -> Cow<'q, str> {
|
||||
pre_process_pattern(self.expando_char(), query)
|
||||
}
|
||||
|
||||
fn kind_to_id(&self, kind: &str) -> u16 {
|
||||
self.get_ts_language().id_for_node_kind(kind, true)
|
||||
}
|
||||
|
||||
fn field_to_id(&self, field: &str) -> Option<u16> {
|
||||
self
|
||||
.get_ts_language()
|
||||
.field_id_for_name(field)
|
||||
.map(|f| f.get())
|
||||
}
|
||||
|
||||
fn build_pattern(&self, builder: &PatternBuilder) -> Result<Pattern, PatternError> {
|
||||
builder.build(|src| StrDoc::try_new(src, *self))
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageExt for Html {
|
||||
fn get_ts_language(&self) -> TSLanguage {
|
||||
parsers::language_html()
|
||||
}
|
||||
|
||||
fn injectable_languages(&self) -> Option<&'static [&'static str]> {
|
||||
Some(&["css", "js", "ts", "tsx", "scss", "less", "stylus", "coffee"])
|
||||
}
|
||||
|
||||
fn extract_injections<L: LanguageExt>(
|
||||
&self,
|
||||
root: Node<StrDoc<L>>,
|
||||
) -> HashMap<String, Vec<TSRange>> {
|
||||
let lang = root.lang();
|
||||
let mut map = HashMap::new();
|
||||
let matcher = KindMatcher::new("script_element", lang.clone());
|
||||
for script in root.find_all(matcher) {
|
||||
let injected = find_html_lang(&script).unwrap_or_else(|| "js".into());
|
||||
let content = script.children().find(|c| c.kind() == "raw_text");
|
||||
if let Some(content) = content {
|
||||
map.entry(injected)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(node_to_range(&content));
|
||||
}
|
||||
}
|
||||
let matcher = KindMatcher::new("style_element", lang.clone());
|
||||
for style in root.find_all(matcher) {
|
||||
let injected = find_html_lang(&style).unwrap_or_else(|| "css".into());
|
||||
let content = style.children().find(|c| c.kind() == "raw_text");
|
||||
if let Some(content) = content {
|
||||
map.entry(injected)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(node_to_range(&content));
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
||||
}
|
||||
|
||||
fn find_html_lang<D: Doc>(node: &Node<D>) -> Option<String> {
|
||||
let html = node.lang();
|
||||
let attr_matcher = KindMatcher::new("attribute", html.clone());
|
||||
let name_matcher = KindMatcher::new("attribute_name", html.clone());
|
||||
let val_matcher = KindMatcher::new("attribute_value", html.clone());
|
||||
node.find_all(attr_matcher).find_map(|attr| {
|
||||
let name = attr.find(&name_matcher)?;
|
||||
if name.text() != "lang" {
|
||||
return None;
|
||||
}
|
||||
let val = attr.find(&val_matcher)?;
|
||||
Some(val.text().to_string())
|
||||
})
|
||||
}
|
||||
|
||||
fn node_to_range<D: Doc>(node: &Node<D>) -> TSRange {
|
||||
let r = node.range();
|
||||
let start = node.start_pos();
|
||||
let sp = start.byte_point();
|
||||
let sp = tree_sitter::Point::new(sp.0, sp.1);
|
||||
let end = node.end_pos();
|
||||
let ep = end.byte_point();
|
||||
let ep = tree_sitter::Point::new(ep.0, ep.1);
|
||||
TSRange { start_byte: r.start, end_byte: r.end, start_point: sp, end_point: ep }
|
||||
}
|
||||
|
||||
// ── SupportLang enum ────────────────────────────────────────────────────
|
||||
|
||||
/// All supported languages for ast-grep structural search/replace.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum SupportLang {
|
||||
Bash,
|
||||
C,
|
||||
Cpp,
|
||||
CSharp,
|
||||
Css,
|
||||
Diff,
|
||||
Elixir,
|
||||
Go,
|
||||
Haskell,
|
||||
Hcl,
|
||||
Html,
|
||||
Java,
|
||||
JavaScript,
|
||||
Json,
|
||||
Julia,
|
||||
Kotlin,
|
||||
Lua,
|
||||
Make,
|
||||
Markdown,
|
||||
Nix,
|
||||
ObjC,
|
||||
Odin,
|
||||
Php,
|
||||
Python,
|
||||
Regex,
|
||||
Ruby,
|
||||
Rust,
|
||||
Scala,
|
||||
Solidity,
|
||||
Starlark,
|
||||
Swift,
|
||||
Toml,
|
||||
Tsx,
|
||||
TypeScript,
|
||||
Verilog,
|
||||
Xml,
|
||||
Yaml,
|
||||
Zig,
|
||||
}
|
||||
|
||||
impl SupportLang {
|
||||
pub const fn all_langs() -> &'static [Self] {
|
||||
use SupportLang::*;
|
||||
&[
|
||||
Bash, C, Cpp, CSharp, Css, Diff, Elixir, Go, Haskell, Hcl, Html, Java, JavaScript, Json,
|
||||
Julia, Kotlin, Lua, Make, Markdown, Nix, ObjC, Odin, Php, Python, Regex, Ruby, Rust,
|
||||
Scala, Solidity, Starlark, Swift, Toml, Tsx, TypeScript, Verilog, Xml, Yaml, Zig,
|
||||
]
|
||||
}
|
||||
|
||||
/// The canonical lowercase name used as a stable key in alias maps,
|
||||
/// file-type inference results, and error messages.
|
||||
pub const fn canonical_name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Bash => "bash",
|
||||
Self::C => "c",
|
||||
Self::Cpp => "cpp",
|
||||
Self::CSharp => "csharp",
|
||||
Self::Css => "css",
|
||||
Self::Diff => "diff",
|
||||
Self::Elixir => "elixir",
|
||||
Self::Go => "go",
|
||||
Self::Haskell => "haskell",
|
||||
Self::Hcl => "hcl",
|
||||
Self::Html => "html",
|
||||
Self::Java => "java",
|
||||
Self::JavaScript => "javascript",
|
||||
Self::Json => "json",
|
||||
Self::Julia => "julia",
|
||||
Self::Kotlin => "kotlin",
|
||||
Self::Lua => "lua",
|
||||
Self::Make => "make",
|
||||
Self::Markdown => "markdown",
|
||||
Self::Nix => "nix",
|
||||
Self::ObjC => "objc",
|
||||
Self::Odin => "odin",
|
||||
Self::Php => "php",
|
||||
Self::Python => "python",
|
||||
Self::Regex => "regex",
|
||||
Self::Ruby => "ruby",
|
||||
Self::Rust => "rust",
|
||||
Self::Scala => "scala",
|
||||
Self::Solidity => "solidity",
|
||||
Self::Starlark => "starlark",
|
||||
Self::Swift => "swift",
|
||||
Self::Toml => "toml",
|
||||
Self::Tsx => "tsx",
|
||||
Self::TypeScript => "typescript",
|
||||
Self::Verilog => "verilog",
|
||||
Self::Xml => "xml",
|
||||
Self::Yaml => "yaml",
|
||||
Self::Zig => "zig",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for SupportLang {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{self:?}")
|
||||
}
|
||||
}
|
||||
|
||||
// ── Dispatch macro ──────────────────────────────────────────────────────
|
||||
|
||||
macro_rules! execute_lang_method {
|
||||
($me:path, $method:ident, $($pname:tt),*) => {
|
||||
use SupportLang as S;
|
||||
match $me {
|
||||
S::Bash => Bash.$method($($pname,)*),
|
||||
S::C => C.$method($($pname,)*),
|
||||
S::Cpp => Cpp.$method($($pname,)*),
|
||||
S::CSharp => CSharp.$method($($pname,)*),
|
||||
S::Css => Css.$method($($pname,)*),
|
||||
S::Diff => Diff.$method($($pname,)*),
|
||||
S::Elixir => Elixir.$method($($pname,)*),
|
||||
S::Go => Go.$method($($pname,)*),
|
||||
S::Haskell => Haskell.$method($($pname,)*),
|
||||
S::Hcl => Hcl.$method($($pname,)*),
|
||||
S::Html => Html.$method($($pname,)*),
|
||||
S::Java => Java.$method($($pname,)*),
|
||||
S::JavaScript => JavaScript.$method($($pname,)*),
|
||||
S::Json => Json.$method($($pname,)*),
|
||||
S::Julia => Julia.$method($($pname,)*),
|
||||
S::Kotlin => Kotlin.$method($($pname,)*),
|
||||
S::Lua => Lua.$method($($pname,)*),
|
||||
S::Make => Make.$method($($pname,)*),
|
||||
S::Markdown => Markdown.$method($($pname,)*),
|
||||
S::Nix => Nix.$method($($pname,)*),
|
||||
S::ObjC => ObjC.$method($($pname,)*),
|
||||
S::Odin => Odin.$method($($pname,)*),
|
||||
S::Php => Php.$method($($pname,)*),
|
||||
S::Python => Python.$method($($pname,)*),
|
||||
S::Regex => Regex.$method($($pname,)*),
|
||||
S::Ruby => Ruby.$method($($pname,)*),
|
||||
S::Rust => Rust.$method($($pname,)*),
|
||||
S::Scala => Scala.$method($($pname,)*),
|
||||
S::Solidity => Solidity.$method($($pname,)*),
|
||||
S::Starlark => Starlark.$method($($pname,)*),
|
||||
S::Swift => Swift.$method($($pname,)*),
|
||||
S::Toml => Toml.$method($($pname,)*),
|
||||
S::Tsx => Tsx.$method($($pname,)*),
|
||||
S::TypeScript => TypeScript.$method($($pname,)*),
|
||||
S::Verilog => Verilog.$method($($pname,)*),
|
||||
S::Xml => Xml.$method($($pname,)*),
|
||||
S::Yaml => Yaml.$method($($pname,)*),
|
||||
S::Zig => Zig.$method($($pname,)*),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! impl_lang_method {
|
||||
($method:ident, ($($pname:tt: $ptype:ty),*) => $return_type:ty) => {
|
||||
#[inline]
|
||||
fn $method(&self, $($pname: $ptype),*) -> $return_type {
|
||||
execute_lang_method! { self, $method, $($pname),* }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl Language for SupportLang {
|
||||
impl_lang_method!(kind_to_id, (kind: &str) => u16);
|
||||
|
||||
impl_lang_method!(field_to_id, (field: &str) => Option<u16>);
|
||||
|
||||
impl_lang_method!(meta_var_char, () => char);
|
||||
|
||||
impl_lang_method!(expando_char, () => char);
|
||||
|
||||
impl_lang_method!(extract_meta_var, (source: &str) => Option<MetaVariable>);
|
||||
|
||||
impl_lang_method!(build_pattern, (builder: &PatternBuilder) => Result<Pattern, PatternError>);
|
||||
|
||||
fn pre_process_pattern<'q>(&self, query: &'q str) -> Cow<'q, str> {
|
||||
execute_lang_method! { self, pre_process_pattern, query }
|
||||
}
|
||||
|
||||
fn from_path<P: AsRef<Path>>(path: P) -> Option<Self> {
|
||||
from_extension(path.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageExt for SupportLang {
|
||||
impl_lang_method!(get_ts_language, () => TSLanguage);
|
||||
|
||||
impl_lang_method!(injectable_languages, () => Option<&'static [&'static str]>);
|
||||
|
||||
fn extract_injections<L: LanguageExt>(
|
||||
&self,
|
||||
root: Node<StrDoc<L>>,
|
||||
) -> HashMap<String, Vec<TSRange>> {
|
||||
match self {
|
||||
Self::Html => Html.extract_injections(root),
|
||||
_ => HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── File extension mapping ──────────────────────────────────────────────
|
||||
|
||||
const fn extensions(lang: SupportLang) -> &'static [&'static str] {
|
||||
use SupportLang::*;
|
||||
match lang {
|
||||
Bash => {
|
||||
&["bash", "bats", "cgi", "command", "env", "fcgi", "ksh", "sh", "tmux", "tool", "zsh"]
|
||||
},
|
||||
C => &["c", "h"],
|
||||
Cpp => &["cc", "hpp", "cpp", "c++", "hh", "cxx", "cu", "ino"],
|
||||
CSharp => &["cs"],
|
||||
Css => &["css", "scss"],
|
||||
Diff => &["diff", "patch"],
|
||||
Elixir => &["ex", "exs"],
|
||||
Go => &["go"],
|
||||
Haskell => &["hs"],
|
||||
Hcl => &["hcl", "tf", "tfvars"],
|
||||
Html => &["html", "htm", "xhtml"],
|
||||
Java => &["java"],
|
||||
JavaScript => &["cjs", "js", "mjs", "jsx"],
|
||||
Json => &["json"],
|
||||
Julia => &["jl"],
|
||||
Kotlin => &["kt", "ktm", "kts"],
|
||||
Lua => &["lua"],
|
||||
Make => &["mk", "mak"],
|
||||
Markdown => &["md", "markdown", "mdx"],
|
||||
Nix => &["nix"],
|
||||
ObjC => &["m"],
|
||||
Odin => &["odin"],
|
||||
Php => &["php"],
|
||||
Python => &["py", "py3", "pyi", "bzl"],
|
||||
Regex => &[], // regex has no file extension
|
||||
Ruby => &["rb", "rbw", "gemspec"],
|
||||
Rust => &["rs"],
|
||||
Scala => &["scala", "sc", "sbt"],
|
||||
Solidity => &["sol"],
|
||||
Starlark => &["star", "bzl"],
|
||||
Swift => &["swift"],
|
||||
Toml => &["toml"],
|
||||
Tsx => &["tsx"],
|
||||
TypeScript => &["ts", "cts", "mts"],
|
||||
Verilog => &["v", "sv", "svh", "vh"],
|
||||
Xml => &["xml", "xsl", "xslt", "svg", "plist"],
|
||||
Yaml => &["yaml", "yml"],
|
||||
Zig => &["zig"],
|
||||
}
|
||||
}
|
||||
|
||||
/// Guess language from file extension.
|
||||
fn from_extension(path: &Path) -> Option<SupportLang> {
|
||||
let ext = path.extension()?.to_str()?;
|
||||
// Special cases: Makefile has no extension
|
||||
if ext.is_empty() {
|
||||
let name = path.file_name()?.to_str()?;
|
||||
return match name {
|
||||
"Makefile" | "makefile" | "GNUmakefile" => Some(SupportLang::Make),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
SupportLang::all_langs()
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|&l| extensions(l).contains(&ext))
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
//! Tree-sitter parser functions for all supported languages.
|
||||
|
||||
use ast_grep_core::tree_sitter::TSLanguage;
|
||||
|
||||
pub fn language_bash() -> TSLanguage {
|
||||
tree_sitter_bash::LANGUAGE.into()
|
||||
}
|
||||
pub fn language_c() -> TSLanguage {
|
||||
tree_sitter_c::LANGUAGE.into()
|
||||
}
|
||||
pub fn language_cpp() -> TSLanguage {
|
||||
tree_sitter_cpp::LANGUAGE.into()
|
||||
}
|
||||
pub fn language_c_sharp() -> TSLanguage {
|
||||
tree_sitter_c_sharp::LANGUAGE.into()
|
||||
}
|
||||
pub fn language_css() -> TSLanguage {
|
||||
tree_sitter_css::LANGUAGE.into()
|
||||
}
|
||||
pub fn language_diff() -> TSLanguage {
|
||||
tree_sitter_diff::LANGUAGE.into()
|
||||
}
|
||||
pub fn language_elixir() -> TSLanguage {
|
||||
tree_sitter_elixir::LANGUAGE.into()
|
||||
}
|
||||
pub fn language_go() -> TSLanguage {
|
||||
tree_sitter_go::LANGUAGE.into()
|
||||
}
|
||||
pub fn language_haskell() -> TSLanguage {
|
||||
tree_sitter_haskell::LANGUAGE.into()
|
||||
}
|
||||
pub fn language_hcl() -> TSLanguage {
|
||||
tree_sitter_hcl::LANGUAGE.into()
|
||||
}
|
||||
pub fn language_html() -> TSLanguage {
|
||||
tree_sitter_html::LANGUAGE.into()
|
||||
}
|
||||
pub fn language_java() -> TSLanguage {
|
||||
tree_sitter_java::LANGUAGE.into()
|
||||
}
|
||||
pub fn language_javascript() -> TSLanguage {
|
||||
tree_sitter_javascript::LANGUAGE.into()
|
||||
}
|
||||
pub fn language_json() -> TSLanguage {
|
||||
tree_sitter_json::LANGUAGE.into()
|
||||
}
|
||||
pub fn language_julia() -> TSLanguage {
|
||||
tree_sitter_julia::LANGUAGE.into()
|
||||
}
|
||||
pub fn language_kotlin() -> TSLanguage {
|
||||
tree_sitter_kotlin::LANGUAGE.into()
|
||||
}
|
||||
pub fn language_lua() -> TSLanguage {
|
||||
tree_sitter_lua::LANGUAGE.into()
|
||||
}
|
||||
pub fn language_make() -> TSLanguage {
|
||||
tree_sitter_make::LANGUAGE.into()
|
||||
}
|
||||
pub fn language_markdown() -> TSLanguage {
|
||||
tree_sitter_md::LANGUAGE.into()
|
||||
}
|
||||
pub fn language_nix() -> TSLanguage {
|
||||
tree_sitter_nix::LANGUAGE.into()
|
||||
}
|
||||
pub fn language_objc() -> TSLanguage {
|
||||
tree_sitter_objc::LANGUAGE.into()
|
||||
}
|
||||
pub fn language_odin() -> TSLanguage {
|
||||
tree_sitter_odin::LANGUAGE.into()
|
||||
}
|
||||
pub fn language_php() -> TSLanguage {
|
||||
tree_sitter_php::LANGUAGE_PHP_ONLY.into()
|
||||
}
|
||||
pub fn language_python() -> TSLanguage {
|
||||
tree_sitter_python::LANGUAGE.into()
|
||||
}
|
||||
pub fn language_regex() -> TSLanguage {
|
||||
tree_sitter_regex::LANGUAGE.into()
|
||||
}
|
||||
pub fn language_ruby() -> TSLanguage {
|
||||
tree_sitter_ruby::LANGUAGE.into()
|
||||
}
|
||||
pub fn language_rust() -> TSLanguage {
|
||||
tree_sitter_rust::LANGUAGE.into()
|
||||
}
|
||||
pub fn language_scala() -> TSLanguage {
|
||||
tree_sitter_scala::LANGUAGE.into()
|
||||
}
|
||||
pub fn language_solidity() -> TSLanguage {
|
||||
tree_sitter_solidity::LANGUAGE.into()
|
||||
}
|
||||
pub fn language_starlark() -> TSLanguage {
|
||||
tree_sitter_starlark::LANGUAGE.into()
|
||||
}
|
||||
pub fn language_swift() -> TSLanguage {
|
||||
tree_sitter_swift::LANGUAGE.into()
|
||||
}
|
||||
pub fn language_toml() -> TSLanguage {
|
||||
tree_sitter_toml_ng::LANGUAGE.into()
|
||||
}
|
||||
pub fn language_tsx() -> TSLanguage {
|
||||
tree_sitter_typescript::LANGUAGE_TSX.into()
|
||||
}
|
||||
pub fn language_typescript() -> TSLanguage {
|
||||
tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()
|
||||
}
|
||||
pub fn language_verilog() -> TSLanguage {
|
||||
tree_sitter_verilog::LANGUAGE.into()
|
||||
}
|
||||
pub fn language_xml() -> TSLanguage {
|
||||
tree_sitter_xml::LANGUAGE_XML.into()
|
||||
}
|
||||
pub fn language_yaml() -> TSLanguage {
|
||||
tree_sitter_yaml::LANGUAGE.into()
|
||||
}
|
||||
pub fn language_zig() -> TSLanguage {
|
||||
tree_sitter_zig::LANGUAGE.into()
|
||||
}
|
||||
@@ -33,6 +33,7 @@ pub mod highlight;
|
||||
pub mod html;
|
||||
pub mod image;
|
||||
pub mod keys;
|
||||
pub mod language;
|
||||
pub mod prof;
|
||||
pub mod projfs_overlay;
|
||||
pub mod ps;
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
|
||||
- Exported `dedupeParseErrors` utility function to deduplicate parse error messages while preserving order
|
||||
|
||||
### Fixed
|
||||
|
||||
- Reduced duplicate parse error messages when multiple patterns fail on the same file
|
||||
- Normalized parse error output in ast-grep to remove pattern-specific prefixes and show only file-level errors
|
||||
|
||||
## [13.7.4] - 2026-03-04
|
||||
### Added
|
||||
|
||||
@@ -16,6 +16,7 @@ import type { ToolSession } from ".";
|
||||
import type { OutputMeta } from "./output-meta";
|
||||
import { hasGlobPathChars, parseSearchPath, resolveToCwd } from "./path-utils";
|
||||
import {
|
||||
dedupeParseErrors,
|
||||
formatCount,
|
||||
formatEmptyMessage,
|
||||
formatErrorMessage,
|
||||
@@ -140,6 +141,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
||||
signal,
|
||||
});
|
||||
|
||||
const dedupedParseErrors = dedupeParseErrors(result.parseErrors);
|
||||
const formatPath = (filePath: string): string => {
|
||||
const cleanPath = filePath.startsWith("/") ? filePath.slice(1) : filePath;
|
||||
if (isDirectory) {
|
||||
@@ -178,15 +180,15 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
||||
filesSearched: result.filesSearched,
|
||||
applied: result.applied,
|
||||
limitReached: result.limitReached,
|
||||
parseErrors: result.parseErrors,
|
||||
parseErrors: dedupedParseErrors,
|
||||
scopePath,
|
||||
files: fileList,
|
||||
fileReplacements: [],
|
||||
};
|
||||
|
||||
if (result.totalReplacements === 0) {
|
||||
const parseMessage = result.parseErrors?.length
|
||||
? `\n${formatParseErrors(result.parseErrors).join("\n")}`
|
||||
const parseMessage = dedupedParseErrors.length
|
||||
? `\n${formatParseErrors(dedupedParseErrors).join("\n")}`
|
||||
: "";
|
||||
return toolResult(baseDetails).text(`No replacements made${parseMessage}`).done();
|
||||
}
|
||||
@@ -258,8 +260,8 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
||||
if (result.limitReached) {
|
||||
outputLines.push("", "Limit reached; narrow path or increase limit.");
|
||||
}
|
||||
if (result.parseErrors?.length) {
|
||||
outputLines.push("", ...formatParseErrors(result.parseErrors));
|
||||
if (dedupedParseErrors.length) {
|
||||
outputLines.push("", ...formatParseErrors(dedupedParseErrors));
|
||||
}
|
||||
|
||||
// Register pending action so `resolve` can apply or discard these previewed changes
|
||||
@@ -281,13 +283,14 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
||||
maxFiles,
|
||||
failOnParseError: false,
|
||||
});
|
||||
const dedupedApplyParseErrors = dedupeParseErrors(applyResult.parseErrors);
|
||||
const appliedDetails: AstEditToolDetails = {
|
||||
totalReplacements: applyResult.totalReplacements,
|
||||
filesTouched: applyResult.filesTouched,
|
||||
filesSearched: applyResult.filesSearched,
|
||||
applied: applyResult.applied,
|
||||
limitReached: applyResult.limitReached,
|
||||
parseErrors: applyResult.parseErrors,
|
||||
parseErrors: dedupedApplyParseErrors,
|
||||
scopePath,
|
||||
files: fileList,
|
||||
fileReplacements,
|
||||
|
||||
@@ -16,6 +16,7 @@ import type { ToolSession } from ".";
|
||||
import type { OutputMeta } from "./output-meta";
|
||||
import { hasGlobPathChars, parseSearchPath, resolveToCwd } from "./path-utils";
|
||||
import {
|
||||
dedupeParseErrors,
|
||||
formatCount,
|
||||
formatEmptyMessage,
|
||||
formatErrorMessage,
|
||||
@@ -133,6 +134,11 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
||||
signal,
|
||||
});
|
||||
|
||||
const normalizedParseErrors = (result.parseErrors ?? []).map(error => {
|
||||
const parseError = error.match(/^.+: (.+: parse error \(syntax tree contains error nodes\))$/);
|
||||
return parseError?.[1] ?? error;
|
||||
});
|
||||
const dedupedParseErrors = dedupeParseErrors(normalizedParseErrors);
|
||||
const formatPath = (filePath: string): string => {
|
||||
const cleanPath = filePath.startsWith("/") ? filePath.slice(1) : filePath;
|
||||
if (isDirectory) {
|
||||
@@ -165,15 +171,15 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
||||
fileCount: result.filesWithMatches,
|
||||
filesSearched: result.filesSearched,
|
||||
limitReached: result.limitReached,
|
||||
parseErrors: result.parseErrors,
|
||||
parseErrors: dedupedParseErrors,
|
||||
scopePath,
|
||||
files: fileList,
|
||||
fileMatches: [],
|
||||
};
|
||||
|
||||
if (result.matches.length === 0) {
|
||||
const parseMessage = result.parseErrors?.length
|
||||
? `\n${formatParseErrors(result.parseErrors).join("\n")}`
|
||||
const parseMessage = dedupedParseErrors.length
|
||||
? `\n${formatParseErrors(dedupedParseErrors).join("\n")}`
|
||||
: "";
|
||||
return toolResult(baseDetails).text(`No matches found${parseMessage}`).done();
|
||||
}
|
||||
@@ -253,8 +259,8 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
||||
if (result.limitReached) {
|
||||
outputLines.push("", "Result limit reached; narrow path pattern or increase limit.");
|
||||
}
|
||||
if (result.parseErrors?.length) {
|
||||
outputLines.push("", ...formatParseErrors(result.parseErrors));
|
||||
if (dedupedParseErrors.length) {
|
||||
outputLines.push("", ...formatParseErrors(dedupedParseErrors));
|
||||
}
|
||||
|
||||
return toolResult(details).text(outputLines.join("\n")).done();
|
||||
|
||||
@@ -533,10 +533,25 @@ export function wrapBrackets(text: string, theme: Theme): string {
|
||||
|
||||
export const PARSE_ERRORS_LIMIT = 20;
|
||||
|
||||
export function dedupeParseErrors(errors: string[] | undefined): string[] {
|
||||
if (!errors || errors.length === 0) return [];
|
||||
const seen = new Set<string>();
|
||||
const deduped: string[] = [];
|
||||
for (const error of errors) {
|
||||
if (seen.has(error)) continue;
|
||||
seen.add(error);
|
||||
deduped.push(error);
|
||||
}
|
||||
return deduped;
|
||||
}
|
||||
|
||||
export function formatParseErrors(errors: string[]): string[] {
|
||||
if (errors.length === 0) return [];
|
||||
const capped = errors.slice(0, PARSE_ERRORS_LIMIT);
|
||||
const deduped = dedupeParseErrors(errors);
|
||||
if (deduped.length === 0) return [];
|
||||
const capped = deduped.slice(0, PARSE_ERRORS_LIMIT);
|
||||
const header =
|
||||
errors.length > PARSE_ERRORS_LIMIT ? `Parse issues (${PARSE_ERRORS_LIMIT} / ${errors.length}):` : "Parse issues:";
|
||||
deduped.length > PARSE_ERRORS_LIMIT
|
||||
? `Parse issues (${PARSE_ERRORS_LIMIT} / ${deduped.length}):`
|
||||
: "Parse issues:";
|
||||
return [header, ...capped.map(err => `- ${err}`)];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
||||
import { createTools, type ToolSession } from "@oh-my-pi/pi-coding-agent/tools";
|
||||
|
||||
function createTestSession(cwd = "/tmp/test", overrides: Partial<ToolSession> = {}): ToolSession {
|
||||
return {
|
||||
cwd,
|
||||
hasUI: true,
|
||||
getSessionFile: () => null,
|
||||
getSessionSpawns: () => "*",
|
||||
settings: Settings.isolated(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("ast_grep parse errors", () => {
|
||||
it("collapses per-pattern parse errors for the same file", async () => {
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ast-grep-parse-"));
|
||||
try {
|
||||
const filePath = path.join(tempDir, "broken.ts");
|
||||
await Bun.write(filePath, "export function broken( { return 1; }");
|
||||
|
||||
const tools = await createTools(createTestSession(tempDir));
|
||||
const tool = tools.find(entry => entry.name === "ast_grep");
|
||||
expect(tool).toBeDefined();
|
||||
|
||||
const result = await tool!.execute("ast-grep-parse", {
|
||||
patterns: ["someUnlikelyCall($A)", "anotherUnlikelyCall($A)"],
|
||||
lang: "typescript",
|
||||
path: filePath,
|
||||
});
|
||||
|
||||
const text = result.content.find(content => content.type === "text")?.text ?? "";
|
||||
const details = result.details as { parseErrors?: string[]; matchCount?: number } | undefined;
|
||||
|
||||
expect(details?.matchCount).toBe(0);
|
||||
expect(text).toContain("No matches found");
|
||||
expect(details?.parseErrors).toHaveLength(1);
|
||||
expect(details?.parseErrors?.[0]).toContain("broken.ts: parse error (syntax tree contains error nodes)");
|
||||
expect(details?.parseErrors?.[0]).not.toContain("someUnlikelyCall($A):");
|
||||
expect(details?.parseErrors?.[0]).not.toContain("anotherUnlikelyCall($A):");
|
||||
expect(text.match(/parse error \(syntax tree contains error nodes\)/g)?.length ?? 0).toBe(1);
|
||||
} finally {
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { dedupeParseErrors, formatParseErrors } from "@oh-my-pi/pi-coding-agent/tools/render-utils";
|
||||
|
||||
describe("parse error formatting", () => {
|
||||
it("deduplicates parse errors while preserving order", () => {
|
||||
const errors = [
|
||||
"foo.ts: parse error (syntax tree contains error nodes)",
|
||||
"foo.ts: parse error (syntax tree contains error nodes)",
|
||||
"bar.ts: parse error (syntax tree contains error nodes)",
|
||||
"foo.ts: parse error (syntax tree contains error nodes)",
|
||||
];
|
||||
|
||||
expect(dedupeParseErrors(errors)).toEqual([
|
||||
"foo.ts: parse error (syntax tree contains error nodes)",
|
||||
"bar.ts: parse error (syntax tree contains error nodes)",
|
||||
]);
|
||||
});
|
||||
|
||||
it("formats deduplicated parse errors", () => {
|
||||
const formatted = formatParseErrors([
|
||||
"foo.ts: parse error (syntax tree contains error nodes)",
|
||||
"foo.ts: parse error (syntax tree contains error nodes)",
|
||||
"bar.ts: parse error (syntax tree contains error nodes)",
|
||||
]);
|
||||
|
||||
expect(formatted).toEqual([
|
||||
"Parse issues:",
|
||||
"- foo.ts: parse error (syntax tree contains error nodes)",
|
||||
"- bar.ts: parse error (syntax tree contains error nodes)",
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user