diff --git a/Cargo.lock b/Cargo.lock index a2e8fc95..3af5a5ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,6 +77,38 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" +[[package]] +name = "camino" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "cc" version = "1.2.48" @@ -300,6 +332,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "plotnik-cli" version = "0.1.0" @@ -314,6 +352,9 @@ dependencies = [ name = "plotnik-langs" version = "0.1.0" dependencies = [ + "cargo_metadata", + "paste", + "plotnik-macros", "tree-sitter", "tree-sitter-bash", "tree-sitter-c", @@ -357,6 +398,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "plotnik-macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.103" @@ -448,6 +498,10 @@ name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] [[package]] name = "serde" diff --git a/Cargo.toml b/Cargo.toml index 6a798a3d..e73f5346 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,4 +2,4 @@ resolver = "2" -members = ["crates/plotnik-cli", "crates/plotnik-lib", "crates/plotnik-langs"] \ No newline at end of file +members = ["crates/plotnik-cli", "crates/plotnik-lib", "crates/plotnik-langs", "crates/plotnik-macros"] \ No newline at end of file diff --git a/crates/plotnik-cli/src/commands/debug/source.rs b/crates/plotnik-cli/src/commands/debug/source.rs index d1262bfb..aae76034 100644 --- a/crates/plotnik-cli/src/commands/debug/source.rs +++ b/crates/plotnik-cli/src/commands/debug/source.rs @@ -25,9 +25,9 @@ pub fn resolve_lang( lang: &Option, _source_text: &Option, source_file: &Option, -) -> Lang { +) -> &'static Lang { if let Some(name) = lang { - return Lang::from_name(name).unwrap_or_else(|| { + return plotnik_langs::from_name(name).unwrap_or_else(|| { eprintln!("error: unknown language: {}", name); std::process::exit(1); }); @@ -37,7 +37,7 @@ pub fn resolve_lang( && path.as_os_str() != "-" && let Some(ext) = path.extension().and_then(|e| e.to_str()) { - return Lang::from_extension(ext).unwrap_or_else(|| { + return plotnik_langs::from_ext(ext).unwrap_or_else(|| { eprintln!( "error: cannot infer language from extension '.{}', use --lang", ext @@ -50,10 +50,10 @@ pub fn resolve_lang( std::process::exit(1); } -pub fn parse_tree(source: &str, lang: Lang) -> tree_sitter::Tree { +pub fn parse_tree(source: &str, lang: &Lang) -> tree_sitter::Tree { let mut parser = tree_sitter::Parser::new(); parser - .set_language(&lang.language()) + .set_language(&lang.ts_lang) .expect("failed to set language"); parser.parse(source, None).expect("failed to parse source") } diff --git a/crates/plotnik-cli/src/commands/langs.rs b/crates/plotnik-cli/src/commands/langs.rs index f12a48fb..daef71cc 100644 --- a/crates/plotnik-cli/src/commands/langs.rs +++ b/crates/plotnik-cli/src/commands/langs.rs @@ -1,20 +1,16 @@ -use plotnik_langs::Lang; - pub fn run() { - let langs = Lang::all(); + let langs = plotnik_langs::all(); println!("Supported languages ({}):", langs.len()); for lang in langs { - println!(" {}", lang.name()); + println!(" {}", lang.name); } } #[cfg(test)] mod tests { - use plotnik_langs::Lang; - - fn smoke_test(lang: Lang, source: &str, expected_root: &str) { + fn smoke_test(lang: &plotnik_langs::Lang, source: &str, expected_root: &str) { let mut parser = tree_sitter::Parser::new(); - parser.set_language(&lang.language()).unwrap(); + parser.set_language(&lang.ts_lang).unwrap(); let tree = parser.parse(source, None).unwrap(); let root = tree.root_node(); assert_eq!(root.kind(), expected_root); @@ -24,56 +20,68 @@ mod tests { #[test] #[cfg(feature = "bash")] fn smoke_parse_bash() { - smoke_test(Lang::Bash, "echo hello", "program"); + smoke_test(plotnik_langs::bash(), "echo hello", "program"); } #[test] #[cfg(feature = "c")] fn smoke_parse_c() { - smoke_test(Lang::C, "int main() { return 0; }", "translation_unit"); + smoke_test( + plotnik_langs::c(), + "int main() { return 0; }", + "translation_unit", + ); } #[test] #[cfg(feature = "cpp")] fn smoke_parse_cpp() { - smoke_test(Lang::Cpp, "int main() { return 0; }", "translation_unit"); + smoke_test( + plotnik_langs::cpp(), + "int main() { return 0; }", + "translation_unit", + ); } #[test] #[cfg(feature = "csharp")] fn smoke_parse_csharp() { - smoke_test(Lang::CSharp, "class Foo { }", "compilation_unit"); + smoke_test(plotnik_langs::csharp(), "class Foo { }", "compilation_unit"); } #[test] #[cfg(feature = "css")] fn smoke_parse_css() { - smoke_test(Lang::Css, "body { color: red; }", "stylesheet"); + smoke_test(plotnik_langs::css(), "body { color: red; }", "stylesheet"); } #[test] #[cfg(feature = "elixir")] fn smoke_parse_elixir() { - smoke_test(Lang::Elixir, "defmodule Foo do end", "source"); + smoke_test(plotnik_langs::elixir(), "defmodule Foo do end", "source"); } #[test] #[cfg(feature = "go")] fn smoke_parse_go() { - smoke_test(Lang::Go, "package main", "source_file"); + smoke_test(plotnik_langs::go(), "package main", "source_file"); } #[test] #[cfg(feature = "haskell")] fn smoke_parse_haskell() { - smoke_test(Lang::Haskell, "main = putStrLn \"hello\"", "haskell"); + smoke_test( + plotnik_langs::haskell(), + "main = putStrLn \"hello\"", + "haskell", + ); } #[test] #[cfg(feature = "hcl")] fn smoke_parse_hcl() { smoke_test( - Lang::Hcl, + plotnik_langs::hcl(), "resource \"aws_instance\" \"x\" {}", "config_file", ); @@ -82,20 +90,20 @@ mod tests { #[test] #[cfg(feature = "html")] fn smoke_parse_html() { - smoke_test(Lang::Html, "", "document"); + smoke_test(plotnik_langs::html(), "", "document"); } #[test] #[cfg(feature = "java")] fn smoke_parse_java() { - smoke_test(Lang::Java, "class Foo {}", "program"); + smoke_test(plotnik_langs::java(), "class Foo {}", "program"); } #[test] #[cfg(feature = "javascript")] fn smoke_parse_javascript() { smoke_test( - Lang::JavaScript, + plotnik_langs::javascript(), "function hello() { return 42; }", "program", ); @@ -104,99 +112,110 @@ mod tests { #[test] #[cfg(feature = "json")] fn smoke_parse_json() { - smoke_test(Lang::Json, r#"{"key": "value"}"#, "document"); + smoke_test(plotnik_langs::json(), r#"{"key": "value"}"#, "document"); } #[test] #[cfg(feature = "kotlin")] fn smoke_parse_kotlin() { - smoke_test(Lang::Kotlin, "fun main() {}", "source_file"); + smoke_test(plotnik_langs::kotlin(), "fun main() {}", "source_file"); } #[test] #[cfg(feature = "lua")] fn smoke_parse_lua() { - smoke_test(Lang::Lua, "print('hello')", "chunk"); + smoke_test(plotnik_langs::lua(), "print('hello')", "chunk"); } #[test] #[cfg(feature = "nix")] fn smoke_parse_nix() { - smoke_test(Lang::Nix, "{ x = 1; }", "source_code"); + smoke_test(plotnik_langs::nix(), "{ x = 1; }", "source_code"); } #[test] #[cfg(feature = "php")] fn smoke_parse_php() { - smoke_test(Lang::Php, ";", "program"); + smoke_test(plotnik_langs::tsx(), "const x =
;", "program"); } #[test] #[cfg(feature = "yaml")] fn smoke_parse_yaml() { - smoke_test(Lang::Yaml, "key: value", "stream"); + smoke_test(plotnik_langs::yaml(), "key: value", "stream"); } #[test] #[cfg(feature = "javascript")] fn lang_from_name() { - assert_eq!(Lang::from_name("js"), Some(Lang::JavaScript)); - assert_eq!(Lang::from_name("JavaScript"), Some(Lang::JavaScript)); - assert_eq!(Lang::from_name("unknown"), None); + assert_eq!(plotnik_langs::from_name("js").unwrap().name, "javascript"); + assert_eq!( + plotnik_langs::from_name("JavaScript").unwrap().name, + "javascript" + ); + assert!(plotnik_langs::from_name("unknown").is_none()); } #[test] #[cfg(feature = "javascript")] fn lang_from_extension() { - assert_eq!(Lang::from_extension("js"), Some(Lang::JavaScript)); - assert_eq!(Lang::from_extension("mjs"), Some(Lang::JavaScript)); + assert_eq!(plotnik_langs::from_ext("js").unwrap().name, "javascript"); + assert_eq!(plotnik_langs::from_ext("mjs").unwrap().name, "javascript"); } } diff --git a/crates/plotnik-langs/Cargo.toml b/crates/plotnik-langs/Cargo.toml index d4ea21e9..820d09dc 100644 --- a/crates/plotnik-langs/Cargo.toml +++ b/crates/plotnik-langs/Cargo.toml @@ -65,6 +65,8 @@ typescript = ["dep:tree-sitter-typescript"] yaml = ["dep:tree-sitter-yaml"] [dependencies] +paste = "1.0" +plotnik-macros = { version = "0.1.0", path = "../plotnik-macros" } tree-sitter = "0.25" tree-sitter-bash = { version = "0.25.0", optional = true } tree-sitter-c = { version = "0.24.0", optional = true } @@ -92,4 +94,7 @@ tree-sitter-swift = { version = "0.7.0", optional = true } tree-sitter-typescript = { version = "0.23.2", optional = true } tree-sitter-yaml = { version = "0.7.0", optional = true } +[build-dependencies] +cargo_metadata = "0.19" + [dev-dependencies] diff --git a/crates/plotnik-langs/build.rs b/crates/plotnik-langs/build.rs new file mode 100644 index 00000000..66cd0cc9 --- /dev/null +++ b/crates/plotnik-langs/build.rs @@ -0,0 +1,95 @@ +use std::path::PathBuf; + +fn main() { + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set"); + let manifest_path = PathBuf::from(&manifest_dir).join("Cargo.toml"); + + let metadata = cargo_metadata::MetadataCommand::new() + .manifest_path(&manifest_path) + .exec() + .expect("failed to run cargo metadata"); + + for package in &metadata.packages { + if !package.name.starts_with("tree-sitter-") { + continue; + } + + let Some(feature_name) = tree_sitter_package_to_feature(&package.name) else { + continue; + }; + + if std::env::var(format!("CARGO_FEATURE_{}", feature_name.to_uppercase())).is_err() { + continue; + } + + let package_root = package + .manifest_path + .parent() + .expect("package has no parent dir"); + + let node_types_paths = get_node_types_paths(&package.name); + for (suffix, rel_path) in node_types_paths { + let node_types_path = package_root.join(rel_path); + + if !node_types_path.exists() { + panic!( + "node-types.json not found for {}: {}", + package.name, node_types_path + ); + } + + let env_var_name = format!( + "PLOTNIK_NODE_TYPES_{}{}", + feature_name.to_uppercase(), + suffix + ); + println!("cargo::rustc-env={}={}", env_var_name, node_types_path); + println!("cargo::rerun-if-changed={}", node_types_path); + } + } + + println!("cargo::rerun-if-changed=build.rs"); + println!("cargo::rerun-if-changed=Cargo.toml"); +} + +fn get_node_types_paths(package_name: &str) -> Vec<(&'static str, &'static str)> { + match package_name { + "tree-sitter-php" => vec![("", "php/src/node-types.json")], + "tree-sitter-typescript" => vec![ + ("", "typescript/src/node-types.json"), + ("_TSX", "tsx/src/node-types.json"), + ], + _ => vec![("", "src/node-types.json")], + } +} + +fn tree_sitter_package_to_feature(package_name: &str) -> Option<&str> { + match package_name { + "tree-sitter-bash" => Some("bash"), + "tree-sitter-c" => Some("c"), + "tree-sitter-cpp" => Some("cpp"), + "tree-sitter-c-sharp" => Some("csharp"), + "tree-sitter-css" => Some("css"), + "tree-sitter-elixir" => Some("elixir"), + "tree-sitter-go" => Some("go"), + "tree-sitter-haskell" => Some("haskell"), + "tree-sitter-hcl" => Some("hcl"), + "tree-sitter-html" => Some("html"), + "tree-sitter-java" => Some("java"), + "tree-sitter-javascript" => Some("javascript"), + "tree-sitter-json" => Some("json"), + "tree-sitter-kotlin-sg" => Some("kotlin"), + "tree-sitter-lua" => Some("lua"), + "tree-sitter-nix" => Some("nix"), + "tree-sitter-php" => Some("php"), + "tree-sitter-python" => Some("python"), + "tree-sitter-ruby" => Some("ruby"), + "tree-sitter-rust" => Some("rust"), + "tree-sitter-scala" => Some("scala"), + "tree-sitter-solidity" => Some("solidity"), + "tree-sitter-swift" => Some("swift"), + "tree-sitter-typescript" => Some("typescript"), + "tree-sitter-yaml" => Some("yaml"), + _ => None, + } +} diff --git a/crates/plotnik-langs/src/lib.rs b/crates/plotnik-langs/src/lib.rs index f2b86221..167e6a2d 100644 --- a/crates/plotnik-langs/src/lib.rs +++ b/crates/plotnik-langs/src/lib.rs @@ -1,356 +1,287 @@ +use std::sync::LazyLock; use tree_sitter::Language; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -#[non_exhaustive] -pub enum Lang { - #[cfg(feature = "bash")] - Bash, - #[cfg(feature = "c")] - C, - #[cfg(feature = "cpp")] - Cpp, - #[cfg(feature = "csharp")] - CSharp, - #[cfg(feature = "css")] - Css, - #[cfg(feature = "elixir")] - Elixir, - #[cfg(feature = "go")] - Go, - #[cfg(feature = "haskell")] - Haskell, - #[cfg(feature = "hcl")] - Hcl, - #[cfg(feature = "html")] - Html, - #[cfg(feature = "java")] - Java, - #[cfg(feature = "javascript")] - JavaScript, - #[cfg(feature = "json")] - Json, - #[cfg(feature = "kotlin")] - Kotlin, - #[cfg(feature = "lua")] - Lua, - #[cfg(feature = "nix")] - Nix, - #[cfg(feature = "php")] - Php, - #[cfg(feature = "python")] - Python, - #[cfg(feature = "ruby")] - Ruby, - #[cfg(feature = "rust")] - Rust, - #[cfg(feature = "scala")] - Scala, - #[cfg(feature = "solidity")] - Solidity, - #[cfg(feature = "swift")] - Swift, - #[cfg(feature = "typescript")] - TypeScript, - #[cfg(feature = "typescript")] - Tsx, - #[cfg(feature = "yaml")] - Yaml, +#[derive(Debug, Clone)] +pub struct Lang { + pub name: &'static str, + pub ts_lang: Language, + pub node_types_size: usize, } -impl Lang { - pub fn language(&self) -> Language { - match self { - #[cfg(feature = "bash")] - Self::Bash => tree_sitter_bash::LANGUAGE.into(), - #[cfg(feature = "c")] - Self::C => tree_sitter_c::LANGUAGE.into(), - #[cfg(feature = "cpp")] - Self::Cpp => tree_sitter_cpp::LANGUAGE.into(), - #[cfg(feature = "csharp")] - Self::CSharp => tree_sitter_c_sharp::LANGUAGE.into(), - #[cfg(feature = "css")] - Self::Css => tree_sitter_css::LANGUAGE.into(), - #[cfg(feature = "elixir")] - Self::Elixir => tree_sitter_elixir::LANGUAGE.into(), - #[cfg(feature = "go")] - Self::Go => tree_sitter_go::LANGUAGE.into(), - #[cfg(feature = "haskell")] - Self::Haskell => tree_sitter_haskell::LANGUAGE.into(), - #[cfg(feature = "hcl")] - Self::Hcl => tree_sitter_hcl::LANGUAGE.into(), - #[cfg(feature = "html")] - Self::Html => tree_sitter_html::LANGUAGE.into(), - #[cfg(feature = "java")] - Self::Java => tree_sitter_java::LANGUAGE.into(), - #[cfg(feature = "javascript")] - Self::JavaScript => tree_sitter_javascript::LANGUAGE.into(), - #[cfg(feature = "json")] - Self::Json => tree_sitter_json::LANGUAGE.into(), - #[cfg(feature = "kotlin")] - Self::Kotlin => tree_sitter_kotlin::LANGUAGE.into(), - #[cfg(feature = "lua")] - Self::Lua => tree_sitter_lua::LANGUAGE.into(), - #[cfg(feature = "nix")] - Self::Nix => tree_sitter_nix::LANGUAGE.into(), - #[cfg(feature = "php")] - Self::Php => tree_sitter_php::LANGUAGE_PHP.into(), - #[cfg(feature = "python")] - Self::Python => tree_sitter_python::LANGUAGE.into(), - #[cfg(feature = "ruby")] - Self::Ruby => tree_sitter_ruby::LANGUAGE.into(), - #[cfg(feature = "rust")] - Self::Rust => tree_sitter_rust::LANGUAGE.into(), - #[cfg(feature = "scala")] - Self::Scala => tree_sitter_scala::LANGUAGE.into(), - #[cfg(feature = "solidity")] - Self::Solidity => tree_sitter_solidity::LANGUAGE.into(), - #[cfg(feature = "swift")] - Self::Swift => tree_sitter_swift::LANGUAGE.into(), - #[cfg(feature = "typescript")] - Self::TypeScript => tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(), - #[cfg(feature = "typescript")] - Self::Tsx => tree_sitter_typescript::LANGUAGE_TSX.into(), - #[cfg(feature = "yaml")] - Self::Yaml => tree_sitter_yaml::LANGUAGE.into(), - #[allow(unreachable_patterns)] - _ => unreachable!("no languages enabled"), - } - } +macro_rules! define_langs { + ( + $( + $fn_name:ident => { + feature: $feature:literal, + name: $name:literal, + ts_lang: $ts_lang:expr, + node_types_key: $node_types_key:literal, + names: [$($alias:literal),* $(,)?], + extensions: [$($ext:literal),* $(,)?] $(,)? + } + ),* $(,)? + ) => { + // Generate node_types_size constants via proc macro + $( + #[cfg(feature = $feature)] + plotnik_macros::generate_node_types_size!($node_types_key); + )* - pub fn from_name(s: &str) -> Option { - match s.to_ascii_lowercase().as_str() { - #[cfg(feature = "bash")] - "bash" | "sh" | "shell" => Some(Self::Bash), - #[cfg(feature = "c")] - "c" => Some(Self::C), - #[cfg(feature = "cpp")] - "cpp" | "c++" | "cxx" | "cc" => Some(Self::Cpp), - #[cfg(feature = "csharp")] - "csharp" | "c#" | "cs" => Some(Self::CSharp), - #[cfg(feature = "css")] - "css" => Some(Self::Css), - #[cfg(feature = "elixir")] - "elixir" | "ex" => Some(Self::Elixir), - #[cfg(feature = "go")] - "go" | "golang" => Some(Self::Go), - #[cfg(feature = "haskell")] - "haskell" | "hs" => Some(Self::Haskell), - #[cfg(feature = "hcl")] - "hcl" | "terraform" | "tf" => Some(Self::Hcl), - #[cfg(feature = "html")] - "html" => Some(Self::Html), - #[cfg(feature = "java")] - "java" => Some(Self::Java), - #[cfg(feature = "javascript")] - "javascript" | "js" | "jsx" => Some(Self::JavaScript), - #[cfg(feature = "json")] - "json" => Some(Self::Json), - #[cfg(feature = "kotlin")] - "kotlin" | "kt" => Some(Self::Kotlin), - #[cfg(feature = "lua")] - "lua" => Some(Self::Lua), - #[cfg(feature = "nix")] - "nix" => Some(Self::Nix), - #[cfg(feature = "php")] - "php" => Some(Self::Php), - #[cfg(feature = "python")] - "python" | "py" => Some(Self::Python), - #[cfg(feature = "ruby")] - "ruby" | "rb" => Some(Self::Ruby), - #[cfg(feature = "rust")] - "rust" | "rs" => Some(Self::Rust), - #[cfg(feature = "scala")] - "scala" => Some(Self::Scala), - #[cfg(feature = "solidity")] - "solidity" | "sol" => Some(Self::Solidity), - #[cfg(feature = "swift")] - "swift" => Some(Self::Swift), - #[cfg(feature = "typescript")] - "typescript" | "ts" => Some(Self::TypeScript), - #[cfg(feature = "typescript")] - "tsx" => Some(Self::Tsx), - #[cfg(feature = "yaml")] - "yaml" | "yml" => Some(Self::Yaml), - _ => None, - } - } + // Generate lazy accessor functions + $( + #[cfg(feature = $feature)] + pub fn $fn_name() -> &'static Lang { + paste::paste! { + static LANG: LazyLock = LazyLock::new(|| Lang { + name: $name, + ts_lang: $ts_lang.into(), + node_types_size: [<$node_types_key:upper _NODE_TYPES_SIZE>], + }); + } + &LANG + } + )* - pub fn from_extension(ext: &str) -> Option { - match ext.to_ascii_lowercase().as_str() { - #[cfg(feature = "bash")] - "sh" | "bash" | "zsh" => Some(Self::Bash), - #[cfg(feature = "c")] - "c" => Some(Self::C), - #[cfg(feature = "cpp")] - "cpp" | "cc" | "cxx" | "hpp" | "hh" | "hxx" | "h++" | "c++" => Some(Self::Cpp), - #[cfg(feature = "csharp")] - "cs" => Some(Self::CSharp), - #[cfg(feature = "css")] - "css" => Some(Self::Css), - #[cfg(feature = "elixir")] - "ex" | "exs" => Some(Self::Elixir), - #[cfg(feature = "go")] - "go" => Some(Self::Go), - #[cfg(feature = "haskell")] - "hs" | "lhs" => Some(Self::Haskell), - #[cfg(feature = "hcl")] - "hcl" | "tf" | "tfvars" => Some(Self::Hcl), - #[cfg(feature = "html")] - "html" | "htm" => Some(Self::Html), - #[cfg(feature = "java")] - "java" => Some(Self::Java), - #[cfg(feature = "javascript")] - "js" | "mjs" | "cjs" | "jsx" => Some(Self::JavaScript), - #[cfg(feature = "json")] - "json" => Some(Self::Json), - #[cfg(feature = "kotlin")] - "kt" | "kts" => Some(Self::Kotlin), - #[cfg(feature = "lua")] - "lua" => Some(Self::Lua), - #[cfg(feature = "nix")] - "nix" => Some(Self::Nix), - #[cfg(feature = "php")] - "php" => Some(Self::Php), - #[cfg(feature = "python")] - "py" | "pyi" | "pyw" => Some(Self::Python), - #[cfg(feature = "ruby")] - "rb" | "rake" | "gemspec" => Some(Self::Ruby), - #[cfg(feature = "rust")] - "rs" => Some(Self::Rust), - #[cfg(feature = "scala")] - "scala" | "sc" => Some(Self::Scala), - #[cfg(feature = "solidity")] - "sol" => Some(Self::Solidity), - #[cfg(feature = "swift")] - "swift" => Some(Self::Swift), - #[cfg(feature = "typescript")] - "ts" | "mts" | "cts" => Some(Self::TypeScript), - #[cfg(feature = "typescript")] - "tsx" => Some(Self::Tsx), - #[cfg(feature = "yaml")] - "yaml" | "yml" => Some(Self::Yaml), - // .h is ambiguous (C or C++), defaulting to C - #[cfg(feature = "c")] - "h" => Some(Self::C), - _ => None, + pub fn from_name(s: &str) -> Option<&'static Lang> { + match s.to_ascii_lowercase().as_str() { + $( + #[cfg(feature = $feature)] + $($alias)|* => Some($fn_name()), + )* + _ => None, + } } - } - pub fn all() -> &'static [Self] { - &[ - #[cfg(feature = "bash")] - Self::Bash, - #[cfg(feature = "c")] - Self::C, - #[cfg(feature = "cpp")] - Self::Cpp, - #[cfg(feature = "csharp")] - Self::CSharp, - #[cfg(feature = "css")] - Self::Css, - #[cfg(feature = "elixir")] - Self::Elixir, - #[cfg(feature = "go")] - Self::Go, - #[cfg(feature = "haskell")] - Self::Haskell, - #[cfg(feature = "hcl")] - Self::Hcl, - #[cfg(feature = "html")] - Self::Html, - #[cfg(feature = "java")] - Self::Java, - #[cfg(feature = "javascript")] - Self::JavaScript, - #[cfg(feature = "json")] - Self::Json, - #[cfg(feature = "kotlin")] - Self::Kotlin, - #[cfg(feature = "lua")] - Self::Lua, - #[cfg(feature = "nix")] - Self::Nix, - #[cfg(feature = "php")] - Self::Php, - #[cfg(feature = "python")] - Self::Python, - #[cfg(feature = "ruby")] - Self::Ruby, - #[cfg(feature = "rust")] - Self::Rust, - #[cfg(feature = "scala")] - Self::Scala, - #[cfg(feature = "solidity")] - Self::Solidity, - #[cfg(feature = "swift")] - Self::Swift, - #[cfg(feature = "typescript")] - Self::TypeScript, - #[cfg(feature = "typescript")] - Self::Tsx, - #[cfg(feature = "yaml")] - Self::Yaml, - ] - } + pub fn from_ext(ext: &str) -> Option<&'static Lang> { + match ext.to_ascii_lowercase().as_str() { + $( + #[cfg(feature = $feature)] + $($ext)|* => Some($fn_name()), + )* + _ => None, + } + } - pub fn name(&self) -> &'static str { - match self { - #[cfg(feature = "bash")] - Self::Bash => "bash", - #[cfg(feature = "c")] - Self::C => "c", - #[cfg(feature = "cpp")] - Self::Cpp => "cpp", - #[cfg(feature = "csharp")] - Self::CSharp => "c_sharp", - #[cfg(feature = "css")] - Self::Css => "css", - #[cfg(feature = "elixir")] - Self::Elixir => "elixir", - #[cfg(feature = "go")] - Self::Go => "go", - #[cfg(feature = "haskell")] - Self::Haskell => "haskell", - #[cfg(feature = "hcl")] - Self::Hcl => "hcl", - #[cfg(feature = "html")] - Self::Html => "html", - #[cfg(feature = "java")] - Self::Java => "java", - #[cfg(feature = "javascript")] - Self::JavaScript => "javascript", - #[cfg(feature = "json")] - Self::Json => "json", - #[cfg(feature = "kotlin")] - Self::Kotlin => "kotlin", - #[cfg(feature = "lua")] - Self::Lua => "lua", - #[cfg(feature = "nix")] - Self::Nix => "nix", - #[cfg(feature = "php")] - Self::Php => "php", - #[cfg(feature = "python")] - Self::Python => "python", - #[cfg(feature = "ruby")] - Self::Ruby => "ruby", - #[cfg(feature = "rust")] - Self::Rust => "rust", - #[cfg(feature = "scala")] - Self::Scala => "scala", - #[cfg(feature = "solidity")] - Self::Solidity => "solidity", - #[cfg(feature = "swift")] - Self::Swift => "swift", - #[cfg(feature = "typescript")] - Self::TypeScript => "typescript", - #[cfg(feature = "typescript")] - Self::Tsx => "tsx", - #[cfg(feature = "yaml")] - Self::Yaml => "yaml", - #[allow(unreachable_patterns)] - _ => unreachable!("no languages enabled"), + pub fn all() -> Vec<&'static Lang> { + vec![ + $( + #[cfg(feature = $feature)] + $fn_name(), + )* + ] } - } + }; +} + +define_langs! { + bash => { + feature: "bash", + name: "bash", + ts_lang: tree_sitter_bash::LANGUAGE, + node_types_key: "bash", + names: ["bash", "sh", "shell"], + extensions: ["sh", "bash", "zsh"], + }, + c => { + feature: "c", + name: "c", + ts_lang: tree_sitter_c::LANGUAGE, + node_types_key: "c", + names: ["c"], + extensions: ["c", "h"], + }, + cpp => { + feature: "cpp", + name: "cpp", + ts_lang: tree_sitter_cpp::LANGUAGE, + node_types_key: "cpp", + names: ["cpp", "c++", "cxx", "cc"], + extensions: ["cpp", "cc", "cxx", "hpp", "hh", "hxx", "h++", "c++"], + }, + csharp => { + feature: "csharp", + name: "c_sharp", + ts_lang: tree_sitter_c_sharp::LANGUAGE, + node_types_key: "csharp", + names: ["csharp", "c#", "cs", "c_sharp"], + extensions: ["cs"], + }, + css => { + feature: "css", + name: "css", + ts_lang: tree_sitter_css::LANGUAGE, + node_types_key: "css", + names: ["css"], + extensions: ["css"], + }, + elixir => { + feature: "elixir", + name: "elixir", + ts_lang: tree_sitter_elixir::LANGUAGE, + node_types_key: "elixir", + names: ["elixir", "ex"], + extensions: ["ex", "exs"], + }, + go => { + feature: "go", + name: "go", + ts_lang: tree_sitter_go::LANGUAGE, + node_types_key: "go", + names: ["go", "golang"], + extensions: ["go"], + }, + haskell => { + feature: "haskell", + name: "haskell", + ts_lang: tree_sitter_haskell::LANGUAGE, + node_types_key: "haskell", + names: ["haskell", "hs"], + extensions: ["hs", "lhs"], + }, + hcl => { + feature: "hcl", + name: "hcl", + ts_lang: tree_sitter_hcl::LANGUAGE, + node_types_key: "hcl", + names: ["hcl", "terraform", "tf"], + extensions: ["hcl", "tf", "tfvars"], + }, + html => { + feature: "html", + name: "html", + ts_lang: tree_sitter_html::LANGUAGE, + node_types_key: "html", + names: ["html", "htm"], + extensions: ["html", "htm"], + }, + java => { + feature: "java", + name: "java", + ts_lang: tree_sitter_java::LANGUAGE, + node_types_key: "java", + names: ["java"], + extensions: ["java"], + }, + javascript => { + feature: "javascript", + name: "javascript", + ts_lang: tree_sitter_javascript::LANGUAGE, + node_types_key: "javascript", + names: ["javascript", "js", "jsx", "ecmascript", "es"], + extensions: ["js", "mjs", "cjs", "jsx"], + }, + json => { + feature: "json", + name: "json", + ts_lang: tree_sitter_json::LANGUAGE, + node_types_key: "json", + names: ["json"], + extensions: ["json"], + }, + kotlin => { + feature: "kotlin", + name: "kotlin", + ts_lang: tree_sitter_kotlin::LANGUAGE, + node_types_key: "kotlin", + names: ["kotlin", "kt"], + extensions: ["kt", "kts"], + }, + lua => { + feature: "lua", + name: "lua", + ts_lang: tree_sitter_lua::LANGUAGE, + node_types_key: "lua", + names: ["lua"], + extensions: ["lua"], + }, + nix => { + feature: "nix", + name: "nix", + ts_lang: tree_sitter_nix::LANGUAGE, + node_types_key: "nix", + names: ["nix"], + extensions: ["nix"], + }, + php => { + feature: "php", + name: "php", + ts_lang: tree_sitter_php::LANGUAGE_PHP, + node_types_key: "php", + names: ["php"], + extensions: ["php"], + }, + python => { + feature: "python", + name: "python", + ts_lang: tree_sitter_python::LANGUAGE, + node_types_key: "python", + names: ["python", "py"], + extensions: ["py", "pyi", "pyw"], + }, + ruby => { + feature: "ruby", + name: "ruby", + ts_lang: tree_sitter_ruby::LANGUAGE, + node_types_key: "ruby", + names: ["ruby", "rb"], + extensions: ["rb", "rake", "gemspec"], + }, + rust => { + feature: "rust", + name: "rust", + ts_lang: tree_sitter_rust::LANGUAGE, + node_types_key: "rust", + names: ["rust", "rs"], + extensions: ["rs"], + }, + scala => { + feature: "scala", + name: "scala", + ts_lang: tree_sitter_scala::LANGUAGE, + node_types_key: "scala", + names: ["scala"], + extensions: ["scala", "sc"], + }, + solidity => { + feature: "solidity", + name: "solidity", + ts_lang: tree_sitter_solidity::LANGUAGE, + node_types_key: "solidity", + names: ["solidity", "sol"], + extensions: ["sol"], + }, + swift => { + feature: "swift", + name: "swift", + ts_lang: tree_sitter_swift::LANGUAGE, + node_types_key: "swift", + names: ["swift"], + extensions: ["swift"], + }, + typescript => { + feature: "typescript", + name: "typescript", + ts_lang: tree_sitter_typescript::LANGUAGE_TYPESCRIPT, + node_types_key: "typescript", + names: ["typescript", "ts"], + extensions: ["ts", "mts", "cts"], + }, + tsx => { + feature: "typescript", + name: "tsx", + ts_lang: tree_sitter_typescript::LANGUAGE_TSX, + node_types_key: "typescript_tsx", + names: ["tsx"], + extensions: ["tsx"], + }, + yaml => { + feature: "yaml", + name: "yaml", + ts_lang: tree_sitter_yaml::LANGUAGE, + node_types_key: "yaml", + names: ["yaml", "yml"], + extensions: ["yaml", "yml"], + }, } #[cfg(test)] @@ -360,15 +291,50 @@ mod tests { #[test] #[cfg(feature = "javascript")] fn lang_from_name() { - assert_eq!(Lang::from_name("js"), Some(Lang::JavaScript)); - assert_eq!(Lang::from_name("JavaScript"), Some(Lang::JavaScript)); - assert_eq!(Lang::from_name("unknown"), None); + assert_eq!(from_name("js").unwrap().name, "javascript"); + assert_eq!(from_name("JavaScript").unwrap().name, "javascript"); + assert!(from_name("unknown").is_none()); + } + + #[test] + #[cfg(feature = "go")] + fn lang_from_name_golang() { + assert_eq!(from_name("go").unwrap().name, "go"); + assert_eq!(from_name("golang").unwrap().name, "go"); + assert_eq!(from_name("GOLANG").unwrap().name, "go"); } #[test] #[cfg(feature = "javascript")] fn lang_from_extension() { - assert_eq!(Lang::from_extension("js"), Some(Lang::JavaScript)); - assert_eq!(Lang::from_extension("mjs"), Some(Lang::JavaScript)); + assert_eq!(from_ext("js").unwrap().name, "javascript"); + assert_eq!(from_ext("mjs").unwrap().name, "javascript"); + } + + #[test] + #[cfg(feature = "typescript")] + fn typescript_and_tsx() { + assert_eq!(typescript().name, "typescript"); + assert_eq!(tsx().name, "tsx"); + assert_eq!(from_ext("ts").unwrap().name, "typescript"); + assert_eq!(from_ext("tsx").unwrap().name, "tsx"); + } + + #[test] + #[cfg(feature = "javascript")] + fn node_types_size_matches_runtime() { + let runtime = std::fs::read_to_string(env!("PLOTNIK_NODE_TYPES_JAVASCRIPT")) + .unwrap() + .len(); + assert_eq!(javascript().node_types_size, runtime); + } + + #[test] + fn all_returns_enabled_langs() { + let langs = all(); + assert!(!langs.is_empty()); + for lang in &langs { + assert!(!lang.name.is_empty()); + } } } diff --git a/crates/plotnik-macros/Cargo.toml b/crates/plotnik-macros/Cargo.toml new file mode 100644 index 00000000..c9073c0f --- /dev/null +++ b/crates/plotnik-macros/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "plotnik-macros" +version = "0.1.0" +edition = "2024" +license = "MIT" +description = "Procedural macros for Plotnik" +repository = "https://github.com/plotnik-lang/plotnik" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1" +quote = "1" +syn = "2" \ No newline at end of file diff --git a/crates/plotnik-macros/src/lib.rs b/crates/plotnik-macros/src/lib.rs new file mode 100644 index 00000000..db35dd63 --- /dev/null +++ b/crates/plotnik-macros/src/lib.rs @@ -0,0 +1,30 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{LitStr, parse_macro_input}; + +#[proc_macro] +pub fn generate_node_types_size(input: TokenStream) -> TokenStream { + let lang = parse_macro_input!(input as LitStr).value(); + let env_var = format!("PLOTNIK_NODE_TYPES_{}", lang.to_uppercase()); + + let path = std::env::var(&env_var).unwrap_or_else(|_| { + panic!( + "Environment variable {} not set. Is build.rs configured correctly?", + env_var + ) + }); + + let size = std::fs::read_to_string(&path) + .unwrap_or_else(|e| panic!("Failed to read {}: {}", path, e)) + .len(); + + let const_name = syn::Ident::new( + &format!("{}_NODE_TYPES_SIZE", lang.to_uppercase()), + proc_macro2::Span::call_site(), + ); + + quote! { + pub const #const_name: usize = #size; + } + .into() +}