tree-sitter-batch 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 WharfLab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,150 @@
1
+ # tree-sitter-batch
2
+
3
+ Windows Batch/CMD grammar for [tree-sitter](https://github.com/tree-sitter/tree-sitter).
4
+
5
+ Parses `.bat` and `.cmd` files into a concrete syntax tree for syntax highlighting, code navigation, and analysis.
6
+
7
+ ## Features
8
+
9
+ - **Control flow** — `IF`/`ELSE` (EXIST, DEFINED, ERRORLEVEL, comparison with NOT), `FOR` (/D /R /L /F), `GOTO`, `CALL`
10
+ - **Variables** — `SET` (plain, `/A` arithmetic, `/P` prompt), `%VAR%`, `!VAR!`, `%%i`, `%~dp0`, `%VAR:old=new%`
11
+ - **Operators** — pipes `|`, redirects `>` `>>` `2>` `2>&1`, conditional `&&` `||`
12
+ - **Structure** — labels `:name`, comments `REM` `::`, parenthesized blocks, `@ECHO OFF`
13
+ - **Scope** — `SETLOCAL`/`ENDLOCAL` with `ENABLEDELAYEDEXPANSION`
14
+ - **Case-insensitive** — all keywords match regardless of casing
15
+
16
+ ## Example
17
+
18
+ ```batch
19
+ @echo off
20
+ REM Build script
21
+ setlocal enabledelayedexpansion
22
+
23
+ set "PROJECT=MyApp"
24
+ set /a VERSION=1
25
+
26
+ if not exist "dist" (
27
+ mkdir dist
28
+ )
29
+
30
+ for %%f in (src\*.txt) do (
31
+ copy "%%f" "dist\"
32
+ )
33
+
34
+ if %ERRORLEVEL% == 0 (
35
+ echo Build successful
36
+ ) else (
37
+ echo Build failed
38
+ exit /b 1
39
+ )
40
+
41
+ exit /b 0
42
+ ```
43
+
44
+ Parsed tree:
45
+
46
+ ```
47
+ (program
48
+ (echo_off)
49
+ (comment)
50
+ (setlocal_stmt)
51
+ (variable_assignment)
52
+ (variable_assignment)
53
+ (if_stmt
54
+ (string)
55
+ (parenthesized
56
+ (cmd (command_name) (argument_list (argument_value)))))
57
+ (for_stmt
58
+ (for_variable)
59
+ (for_set)
60
+ (parenthesized
61
+ (cmd (command_name) (argument_list (string) (string)))))
62
+ (if_stmt
63
+ (variable_reference)
64
+ (comparison_op)
65
+ (integer)
66
+ (parenthesized
67
+ (cmd (command_name) (argument_list (argument_value) (argument_value))))
68
+ (else_clause
69
+ (parenthesized
70
+ (cmd (command_name) (argument_list (argument_value) (argument_value)))
71
+ (exit_stmt (integer)))))
72
+ (exit_stmt (integer)))
73
+ ```
74
+
75
+ ## Installation
76
+
77
+ ### npm
78
+
79
+ ```sh
80
+ npm install tree-sitter-batch
81
+ ```
82
+
83
+ ### Cargo
84
+
85
+ ```sh
86
+ cargo add tree-sitter-batch
87
+ ```
88
+
89
+ ### PyPI
90
+
91
+ ```sh
92
+ pip install tree-sitter-batch
93
+ ```
94
+
95
+ ### Go
96
+
97
+ ```go
98
+ import tree_sitter_batch "github.com/wharflab/tree-sitter-batch/bindings/go"
99
+ ```
100
+
101
+ ## Usage
102
+
103
+ ### Node.js
104
+
105
+ ```javascript
106
+ import Parser from "tree-sitter";
107
+ import Batch from "tree-sitter-batch";
108
+
109
+ const parser = new Parser();
110
+ parser.setLanguage(Batch);
111
+
112
+ const tree = parser.parse(`@echo off\necho Hello World\n`);
113
+ console.log(tree.rootNode.toString());
114
+ ```
115
+
116
+ ### Rust
117
+
118
+ ```rust
119
+ let mut parser = tree_sitter::Parser::new();
120
+ let language = tree_sitter_batch::LANGUAGE;
121
+ parser.set_language(&language.into()).unwrap();
122
+
123
+ let tree = parser.parse("@echo off\necho Hello\n", None).unwrap();
124
+ println!("{}", tree.root_node().to_sexp());
125
+ ```
126
+
127
+ ### Python
128
+
129
+ ```python
130
+ from tree_sitter import Language, Parser
131
+ import tree_sitter_batch
132
+
133
+ parser = Parser(Language(tree_sitter_batch.language()))
134
+ tree = parser.parse(b"@echo off\necho Hello\n")
135
+ print(tree.root_node.sexp())
136
+ ```
137
+
138
+ ## Syntax Highlighting
139
+
140
+ The grammar ships with a `queries/highlights.scm` file for use in editors that support tree-sitter highlighting (Neovim, Helix, Zed, etc.).
141
+
142
+ ## References
143
+
144
+ - Grammar informed by [Blinter](https://github.com/tboy1337/Blinter) batch file linter (159 rules)
145
+ - [SS64 CMD reference](https://ss64.com/nt/)
146
+ - [Microsoft CMD documentation](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/windows-commands)
147
+
148
+ ## License
149
+
150
+ [MIT](LICENSE)
package/binding.gyp ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "targets": [
3
+ {
4
+ "target_name": "tree_sitter_batch_binding",
5
+ "dependencies": [
6
+ "<!(node -p \"require('node-addon-api').targets\"):node_addon_api_except",
7
+ ],
8
+ "include_dirs": [
9
+ "src",
10
+ ],
11
+ "sources": [
12
+ "bindings/node/binding.cc",
13
+ "src/parser.c",
14
+ ],
15
+ "variables": {
16
+ "has_scanner": "<!(node -p \"fs.existsSync('src/scanner.c')\")"
17
+ },
18
+ "conditions": [
19
+ ["has_scanner=='true'", {
20
+ "sources+": ["src/scanner.c"],
21
+ }],
22
+ ["OS!='win'", {
23
+ "cflags_c": [
24
+ "-std=c11",
25
+ ],
26
+ }, { # OS == "win"
27
+ "cflags_c": [
28
+ "/std:c11",
29
+ "/utf-8",
30
+ ],
31
+ }],
32
+ ],
33
+ }
34
+ ]
35
+ }
@@ -0,0 +1,19 @@
1
+ #include <napi.h>
2
+
3
+ typedef struct TSLanguage TSLanguage;
4
+
5
+ extern "C" TSLanguage *tree_sitter_batch();
6
+
7
+ // "tree-sitter", "language" hashed with BLAKE2
8
+ const napi_type_tag LANGUAGE_TYPE_TAG = {
9
+ 0x8AF2E5212AD58ABF, 0xD5006CAD83ABBA16
10
+ };
11
+
12
+ Napi::Object Init(Napi::Env env, Napi::Object exports) {
13
+ auto language = Napi::External<TSLanguage>::New(env, tree_sitter_batch());
14
+ language.TypeTag(&LANGUAGE_TYPE_TAG);
15
+ exports["language"] = language;
16
+ return exports;
17
+ }
18
+
19
+ NODE_API_MODULE(tree_sitter_batch_binding, Init)
@@ -0,0 +1,60 @@
1
+ type BaseNode = {
2
+ type: string;
3
+ named: boolean;
4
+ };
5
+
6
+ type ChildNode = {
7
+ multiple: boolean;
8
+ required: boolean;
9
+ types: BaseNode[];
10
+ };
11
+
12
+ type NodeInfo =
13
+ | (BaseNode & {
14
+ subtypes: BaseNode[];
15
+ })
16
+ | (BaseNode & {
17
+ fields: { [name: string]: ChildNode };
18
+ children: ChildNode[];
19
+ });
20
+
21
+ /**
22
+ * The tree-sitter language object for this grammar.
23
+ *
24
+ * @see {@linkcode https://tree-sitter.github.io/node-tree-sitter/interfaces/Parser.Language.html Parser.Language}
25
+ *
26
+ * @example
27
+ * import Parser from "tree-sitter";
28
+ * import Batch from "tree-sitter-batch";
29
+ *
30
+ * const parser = new Parser();
31
+ * parser.setLanguage(Batch);
32
+ */
33
+ declare const binding: {
34
+ /**
35
+ * The inner language object.
36
+ * @private
37
+ */
38
+ language: unknown;
39
+
40
+ /**
41
+ * The content of the `node-types.json` file for this grammar.
42
+ *
43
+ * @see {@linkplain https://tree-sitter.github.io/tree-sitter/using-parsers/6-static-node-types Static Node Types}
44
+ */
45
+ nodeTypeInfo: NodeInfo[];
46
+
47
+ /** The syntax highlighting query for this grammar. */
48
+ HIGHLIGHTS_QUERY?: string;
49
+
50
+ /** The language injection query for this grammar. */
51
+ INJECTIONS_QUERY?: string;
52
+
53
+ /** The local variable query for this grammar. */
54
+ LOCALS_QUERY?: string;
55
+
56
+ /** The symbol tagging query for this grammar. */
57
+ TAGS_QUERY?: string;
58
+ };
59
+
60
+ export default binding;
@@ -0,0 +1,37 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ const root = fileURLToPath(new URL("../..", import.meta.url));
5
+
6
+ const binding = typeof process.versions.bun === "string"
7
+ // Support `bun build --compile` by being statically analyzable enough to find the .node file at build-time
8
+ ? await import(`${root}/prebuilds/${process.platform}-${process.arch}/tree-sitter-batch.node`)
9
+ : (await import("node-gyp-build")).default(root);
10
+
11
+ try {
12
+ const nodeTypes = await import(`${root}/src/node-types.json`, { with: { type: "json" } });
13
+ binding.nodeTypeInfo = nodeTypes.default;
14
+ } catch { }
15
+
16
+ const queries = [
17
+ ["HIGHLIGHTS_QUERY", `${root}/queries/highlights.scm`],
18
+ ["INJECTIONS_QUERY", `${root}/queries/injections.scm`],
19
+ ["LOCALS_QUERY", `${root}/queries/locals.scm`],
20
+ ["TAGS_QUERY", `${root}/queries/tags.scm`],
21
+ ];
22
+
23
+ for (const [prop, path] of queries) {
24
+ Object.defineProperty(binding, prop, {
25
+ configurable: true,
26
+ enumerable: true,
27
+ get() {
28
+ delete binding[prop];
29
+ try {
30
+ binding[prop] = readFileSync(path, "utf8");
31
+ } catch { }
32
+ return binding[prop];
33
+ }
34
+ });
35
+ }
36
+
37
+ export default binding;
package/grammar.js ADDED
@@ -0,0 +1,116 @@
1
+ const ci = (word) => new RegExp(word.split('').map((c) => /[a-zA-Z]/.test(c) ? `[${c.toLowerCase()}${c.toUpperCase()}]` : c).join(''));
2
+ const kw = (word) => token(prec(10, ci(word)));
3
+
4
+ export default grammar({
5
+ name: 'batch',
6
+ extras: () => [/[ \t]/],
7
+ rules: {
8
+ program: ($) => repeat(choice(seq($._stmt, /\r?\n/), /\r?\n/)),
9
+ _stmt: ($) => choice(
10
+ $.echo_off, $.comment, $.label, $.variable_assignment,
11
+ $.if_stmt, $.goto_stmt, $.call_stmt, $.exit_stmt,
12
+ $.setlocal_stmt, $.endlocal_stmt, $.for_stmt,
13
+ $.redirect_stmt, $.pipe_stmt, $.cond_exec,
14
+ $.parenthesized, $.cmd,
15
+ ),
16
+ echo_off: () => prec(10, seq('@', kw('echo'), choice(kw('off'), kw('on')))),
17
+ comment: () => token(prec(10, choice(
18
+ seq(optional('@'), /[rR][eE][mM]/, optional(seq(/[ \t]/, /[^\r\n]*/))),
19
+ seq('::', /[^\r\n]*/),
20
+ ))),
21
+ label: () => token(seq(':', /[a-zA-Z_][a-zA-Z0-9_-]*/)),
22
+ variable_assignment: () => prec(8, seq(
23
+ optional('@'), kw('set'),
24
+ optional(seq(/[ \t]+/, /\/[aApP]/)), /[ \t]+/,
25
+ choice(
26
+ seq('"', /[a-zA-Z_][a-zA-Z0-9_]*/, '=', optional(/[^"\r\n]*/), '"'),
27
+ seq(/[a-zA-Z_][a-zA-Z0-9_]*/, '=', optional(/[^\r\n]*/)),
28
+ ),
29
+ )),
30
+ if_stmt: ($) => prec.right(8, seq(
31
+ optional('@'), kw('if'),
32
+ optional(kw('not')),
33
+ choice(
34
+ seq(kw('exist'), choice($.string, $.variable_reference)),
35
+ seq(kw('defined'), /[a-zA-Z_][a-zA-Z0-9_]*/),
36
+ seq(kw('errorlevel'), $.integer),
37
+ seq(
38
+ choice($.string, $.variable_reference, $.integer),
39
+ $.comparison_op,
40
+ choice($.string, $.variable_reference, $.integer),
41
+ ),
42
+ ),
43
+ choice(
44
+ // Parenthesized form: supports else clause
45
+ seq($.parenthesized, optional($.else_clause)),
46
+ // Inline command form: no else (ambiguous)
47
+ $.cmd,
48
+ ),
49
+ )),
50
+ else_clause: ($) => prec.right(8, seq(
51
+ kw('else'),
52
+ choice($.parenthesized, $.cmd),
53
+ )),
54
+ comparison_op: () => token(prec(10, choice('==', ci('equ'), ci('neq'), ci('lss'), ci('leq'), ci('gtr'), ci('geq')))),
55
+ goto_stmt: () => prec(8, seq(
56
+ optional('@'), kw('goto'),
57
+ choice(token(prec(10, ci(':eof'))), token(seq(optional(':'), /[a-zA-Z_][a-zA-Z0-9_-]*/))),
58
+ )),
59
+ call_stmt: ($) => prec(8, seq(
60
+ optional('@'), kw('call'),
61
+ choice(token(seq(':', /[a-zA-Z_][a-zA-Z0-9_-]*/)), $.command_name),
62
+ optional($.argument_list),
63
+ )),
64
+ exit_stmt: ($) => prec(8, seq(
65
+ optional('@'), kw('exit'),
66
+ optional(choice(
67
+ seq(kw('/b'), choice($.integer, $.variable_reference)),
68
+ kw('/b'),
69
+ $.integer,
70
+ )),
71
+ )),
72
+ setlocal_stmt: () => prec(8, seq(
73
+ optional('@'), kw('setlocal'),
74
+ optional(choice(kw('enabledelayedexpansion'), kw('disabledelayedexpansion'), kw('enableextensions'), kw('disableextensions'))),
75
+ )),
76
+ endlocal_stmt: () => prec(8, seq(optional('@'), kw('endlocal'))),
77
+ for_stmt: ($) => prec(8, seq(
78
+ optional('@'), kw('for'),
79
+ optional($.for_options), $.for_variable,
80
+ kw('in'), '(', optional($.for_set), ')', kw('do'),
81
+ choice($.parenthesized, $.cmd),
82
+ )),
83
+ for_options: () => token(prec(10, choice(ci('/d'), seq(ci('/r'), optional(seq(/[ \t]+/, /[^\s]+/))), ci('/l'), seq(ci('/f'), optional(seq(/[ \t]+/, '"', /[^"]*/, '"')))))),
84
+ for_variable: () => token(seq('%%', optional('~'), /[a-zA-Z]/)),
85
+ for_set: () => /[^)\r\n]+/,
86
+ parenthesized: ($) => seq('(', repeat(choice(seq($._stmt, /\r?\n/), /\r?\n/)), ')'),
87
+ redirect_stmt: ($) => prec.right(4, seq(choice($.cmd, $.parenthesized), $.redirection)),
88
+ redirection: ($) => prec.right(seq(
89
+ optional(/[0-2]/), $.redirect_op, $.redirect_target,
90
+ optional(seq(optional(/[0-2]/), $.redirect_op, $.redirect_target)),
91
+ )),
92
+ redirect_op: () => token(choice('2>&1', '>&1', '2>>', '2>', '>>', '>', '<')),
93
+ redirect_target: () => token(choice(ci('nul'), ci('con'), /[^\s|&><\r\n]+/)),
94
+ pipe_stmt: ($) => prec.left(3, seq(choice($.cmd, $.parenthesized), '|', choice($.cmd, $.parenthesized))),
95
+ cond_exec: ($) => choice(
96
+ prec.left(2, seq(choice($.cmd, $.parenthesized), '&&', choice($.cmd, $.parenthesized))),
97
+ prec.left(1, seq(choice($.cmd, $.parenthesized), '||', choice($.cmd, $.parenthesized))),
98
+ ),
99
+ variable_reference: () => token(choice(
100
+ seq('%', /[a-zA-Z_][a-zA-Z0-9_]*/, '%'),
101
+ seq('%~', /[a-zA-Z]*/, /[0-9]/),
102
+ seq('%', /[0-9]/),
103
+ seq('%%', optional('~'), /[a-zA-Z]/),
104
+ seq('!', /[a-zA-Z_][a-zA-Z0-9_]*/, '!'),
105
+ seq('%', /[a-zA-Z_][a-zA-Z0-9_]*/, ':', /[^%]+/, '%'),
106
+ )),
107
+ string: () => token(seq('"', /[^"\r\n]*/, '"')),
108
+ cmd: ($) => prec.right(5, seq(optional('@'), $.command_name, optional($.argument_list))),
109
+ command_name: () => /[a-zA-Z_][a-zA-Z0-9_.-]*/,
110
+ argument_list: ($) => prec.right(repeat1($._arg)),
111
+ _arg: ($) => choice($.string, $.variable_reference, $.command_option, $.argument_value),
112
+ command_option: () => token(seq('/', /[a-zA-Z_?][a-zA-Z0-9_:]*/)),
113
+ argument_value: () => /[^\s|&><"\r\n%!][^\s|&><"\r\n]*/,
114
+ integer: () => /[0-9]+/,
115
+ },
116
+ });
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "tree-sitter-batch",
3
+ "version": "0.1.0",
4
+ "description": "A Windows Batch/CMD grammar for tree-sitter",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/wharflab/tree-sitter-batch.git"
9
+ },
10
+ "license": "MIT",
11
+ "main": "bindings/node",
12
+ "types": "bindings/node",
13
+ "keywords": [
14
+ "incremental",
15
+ "parsing",
16
+ "tree-sitter",
17
+ "batch",
18
+ "cmd",
19
+ "bat"
20
+ ],
21
+ "files": [
22
+ "grammar.js",
23
+ "tree-sitter.json",
24
+ "binding.gyp",
25
+ "prebuilds/**",
26
+ "bindings/node/*",
27
+ "!bindings/node/*_test.js",
28
+ "queries/*",
29
+ "src/**",
30
+ "*.wasm"
31
+ ],
32
+ "dependencies": {
33
+ "node-addon-api": "^8.5.0",
34
+ "node-gyp-build": "^4.8.4"
35
+ },
36
+ "devDependencies": {
37
+ "eslint": "^9.15.0",
38
+ "eslint-config-treesitter": "^1.0.2",
39
+ "prebuildify": "^6.0.1",
40
+ "tree-sitter-cli": "^0.26.6"
41
+ },
42
+ "peerDependencies": {
43
+ "tree-sitter": "^0.25.0"
44
+ },
45
+ "peerDependenciesMeta": {
46
+ "tree-sitter": {
47
+ "optional": true
48
+ }
49
+ },
50
+ "scripts": {
51
+ "install": "node-gyp-build",
52
+ "prestart": "tree-sitter build --wasm",
53
+ "start": "tree-sitter playground",
54
+ "lint": "eslint grammar.js",
55
+ "test": "node --test bindings/node/*_test.js"
56
+ }
57
+ }
@@ -0,0 +1,51 @@
1
+ ; Echo off/on
2
+ (echo_off) @keyword
3
+
4
+ ; Comments
5
+ (comment) @comment
6
+
7
+ ; Labels
8
+ (label) @label
9
+
10
+ ; Variable assignment
11
+ (variable_assignment) @variable
12
+
13
+ ; IF/FOR/GOTO/CALL statements
14
+ (if_stmt) @keyword
15
+ (for_stmt) @keyword
16
+ (goto_stmt) @keyword
17
+ (call_stmt) @keyword
18
+ (setlocal_stmt) @keyword
19
+ (endlocal_stmt) @keyword
20
+ (exit_stmt) @keyword
21
+
22
+ ; Operators
23
+ (comparison_op) @operator
24
+ (redirect_op) @operator
25
+
26
+ ; Commands
27
+ (command_name) @function
28
+
29
+ ; Variables
30
+ (variable_reference) @variable
31
+
32
+ ; FOR loop variables
33
+ (for_variable) @variable.parameter
34
+
35
+ ; FOR options
36
+ (for_options) @constant
37
+
38
+ ; Strings
39
+ (string) @string
40
+
41
+ ; Numbers
42
+ (integer) @number
43
+
44
+ ; Command options/flags
45
+ (command_option) @constant
46
+
47
+ ; Argument values
48
+ (argument_value) @string
49
+
50
+ ; Redirect targets
51
+ (redirect_target) @string.special