├── .gitignore ├── .npmignore ├── Gruntfile.js ├── Makefile ├── README.md ├── VERSION.yml ├── eslint.yaml ├── package.json ├── smp ├── .gitignore ├── package.json └── sample.js ├── src ├── astq-adapter-asty.js ├── astq-adapter-cheerio.js ├── astq-adapter-graphql.js ├── astq-adapter-json.js ├── astq-adapter-mozast.js ├── astq-adapter-parse5.js ├── astq-adapter-unist.js ├── astq-adapter-xmldom.js ├── astq-adapter.js ├── astq-funcs-std.js ├── astq-funcs.js ├── astq-query-exec.js ├── astq-query-parse.pegjs ├── astq-query-trace.js ├── astq-query.js ├── astq-util.js ├── astq-version.js └── astq.js └── tst └── astq.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | smp/node_modules 3 | lib 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | smp/node_modules 3 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** ASTq -- Abstract Syntax Tree (AST) Query Engine 3 | ** Copyright (c) 2014-2024 Dr. Ralf S. Engelschall 4 | ** 5 | ** Permission is hereby granted, free of charge, to any person obtaining 6 | ** a copy of this software and associated documentation files (the 7 | ** "Software"), to deal in the Software without restriction, including 8 | ** without limitation the rights to use, copy, modify, merge, publish, 9 | ** distribute, sublicense, and/or sell copies of the Software, and to 10 | ** permit persons to whom the Software is furnished to do so, subject to 11 | ** the following conditions: 12 | ** 13 | ** The above copyright notice and this permission notice shall be included 14 | ** in all copies or substantial portions of the Software. 15 | ** 16 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | /* global module: true */ 26 | module.exports = function (grunt) { 27 | grunt.loadNpmTasks("grunt-contrib-clean"); 28 | grunt.loadNpmTasks("grunt-browserify"); 29 | grunt.loadNpmTasks("grunt-mocha-test"); 30 | grunt.loadNpmTasks("grunt-eslint"); 31 | 32 | grunt.initConfig({ 33 | version: grunt.file.readYAML("VERSION.yml"), 34 | eslint: { 35 | options: { 36 | overrideConfigFile: "eslint.yaml" 37 | }, 38 | "astq": [ "src/**/*.js", "tst/**/*.js" ] 39 | }, 40 | browserify: { 41 | "astq-browser": { 42 | files: { 43 | "lib/astq.browser.js": [ "src/**/*.js" ] 44 | }, 45 | options: { 46 | transform: [ 47 | [ "browserify-replace", { replace: [ 48 | { from: /\$major/g, to: "<%= version.major %>" }, 49 | { from: /\$minor/g, to: "<%= version.minor %>" }, 50 | { from: /\$micro/g, to: "<%= version.micro %>" }, 51 | { from: /\$date/g, to: "<%= version.date %>" } 52 | ]}], 53 | [ "babelify", { 54 | presets: [ 55 | [ "@babel/preset-env", { 56 | "targets": { 57 | "browsers": "last 8 versions, > 1%, ie 11" 58 | } 59 | } ] 60 | ] 61 | } ], 62 | "pegjs-otf/transform", 63 | [ "uglifyify", { sourceMap: false, global: true } ] 64 | ], 65 | plugin: [ 66 | "browserify-derequire", 67 | "browserify-header" 68 | ], 69 | browserifyOptions: { 70 | standalone: "ASTQ", 71 | debug: false 72 | } 73 | } 74 | }, 75 | "astq-node": { 76 | files: { 77 | "lib/astq.node.js": [ "src/**/*.js" ] 78 | }, 79 | options: { 80 | transform: [ 81 | [ "browserify-replace", { replace: [ 82 | { from: /\$major/g, to: "<%= version.major %>" }, 83 | { from: /\$minor/g, to: "<%= version.minor %>" }, 84 | { from: /\$micro/g, to: "<%= version.micro %>" }, 85 | { from: /\$date/g, to: "<%= version.date %>" } 86 | ]}], 87 | [ "babelify", { 88 | presets: [ 89 | [ "@babel/preset-env", { 90 | "targets": { 91 | "node": "8.0.0" 92 | } 93 | } ] 94 | ] 95 | } ], 96 | "pegjs-otf/transform" 97 | ], 98 | plugin: [ 99 | "browserify-header" 100 | ], 101 | external: [ 102 | "pegjs-otf", 103 | "pegjs-util", 104 | "asty", 105 | "cache-lru" 106 | ], 107 | browserifyOptions: { 108 | standalone: "ASTQ", 109 | debug: false 110 | } 111 | } 112 | } 113 | }, 114 | mochaTest: { 115 | "astq": { 116 | src: [ "tst/*.js" ] 117 | }, 118 | options: { 119 | reporter: "spec", 120 | timeout: 5*1000 121 | } 122 | }, 123 | clean: { 124 | clean: [], 125 | distclean: [ "node_modules" ] 126 | } 127 | }); 128 | 129 | grunt.registerTask("default", [ "eslint", "browserify", "mochaTest" ]); 130 | grunt.registerTask("test", [ "browserify:astq-node", "mochaTest" ]); 131 | }; 132 | 133 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ## 2 | ## ASTq -- Abstract Syntax Tree (AST) Query Engine 3 | ## Copyright (c) 2014-2024 Dr. Ralf S. Engelschall 4 | ## 5 | ## Permission is hereby granted, free of charge, to any person obtaining 6 | ## a copy of this software and associated documentation files (the 7 | ## "Software"), to deal in the Software without restriction, including 8 | ## without limitation the rights to use, copy, modify, merge, publish, 9 | ## distribute, sublicense, and/or sell copies of the Software, and to 10 | ## permit persons to whom the Software is furnished to do so, subject to 11 | ## the following conditions: 12 | ## 13 | ## The above copyright notice and this permission notice shall be included 14 | ## in all copies or substantial portions of the Software. 15 | ## 16 | ## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ## EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ## MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ## IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ## CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ## TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ## SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | ## 24 | 25 | NPM = npm 26 | GRUNT = ./node_modules/grunt-cli/bin/grunt 27 | 28 | all: build 29 | 30 | bootstrap: 31 | @if [ ! -x $(GRUNT) ]; then $(NPM) install; fi 32 | 33 | build: bootstrap 34 | @$(GRUNT) 35 | 36 | clean: bootstrap 37 | @$(GRUNT) clean:clean 38 | 39 | distclean: bootstrap 40 | @$(GRUNT) clean:clean clean:distclean 41 | 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ASTq 3 | ==== 4 | 5 | Abstract Syntax Tree (AST) Query Engine 6 | 7 | [![github (author stars)](https://img.shields.io/github/stars/rse?logo=github&label=author%20stars&color=%233377aa)](https://github.com/rse) 8 | [![github (author followers)](https://img.shields.io/github/followers/rse?label=author%20followers&logo=github&color=%234477aa)](https://github.com/rse) 9 |
10 | [![npm (project release)](https://img.shields.io/npm/v/astq?logo=npm&label=npm%20release&color=%23cc3333)](https://npmjs.com/astq) 11 | [![npm (project downloads)](https://img.shields.io/npm/dm/asty?logo=npm&label=npm%20downloads&color=%23cc3333)](https://npmjs.com/astq) 12 | 13 | Installation 14 | ------------ 15 | 16 | ```shell 17 | $ npm install astq 18 | ``` 19 | 20 | About 21 | ----- 22 | 23 | ASTq is an Abstract Syntax Tree (AST) query engine library for 24 | JavaScript, i.e., it allows you to query nodes of an arbitary AST-style 25 | hierarchical data structure with the help of a powerful XPath-inspired 26 | query language. ASTq can operate on arbitrary AST-style data structures 27 | through the help of pluggable access adapters. 28 | 29 | Query Language 30 | -------------- 31 | 32 | ASTq uses an XPath-inspired Domain Specific Language (DSL) 33 | for querying the supplied AST-style hierarchical data structure. 34 | 35 | ### By Example 36 | 37 | At its simplest form, a query looks like a POSIX filesystem path: 38 | 39 | Foo/Bar/Quux 40 | 41 | This means: query and return all nodes of type `Quux`, which in turn 42 | are childs of nodes of type `Bar`, which in turn are childs of nodes of 43 | type `Foo`, which in turn has to be the start node. 44 | 45 | A little bit more sophisticated query, showing more features, 46 | like axis, filter and optional whitespaces for padding: 47 | 48 | // Foo [ /Bar [ @bar == 'baz1' || @bar == 'baz2' ] && /Quux ] 49 | 50 | This means: query and return all nodes anywhere under the start node 51 | which are of type `Foo` and which have both childs of type `Bar` -- and 52 | with an attribute `bar` of values `baz1` or `baz2` -- and childs of type 53 | `Quux`. 54 | 55 | ### By Grammar 56 | 57 | In general, a query consists of one or more individual query paths, 58 | separated by comma. A path consists of a mandatory initial query step 59 | and optionally zero or more subsequent query steps. 60 | 61 | The difference between initial and subsequent query steps is that an 62 | initial query step does not need an axis while all subsequent query 63 | steps require it. A query step consists of an (optional) AST node search 64 | axis, a (mandatory) AST node type match, an (optional) result marker "!" 65 | and an (optional) AST node filter expression: 66 | 67 | query ::= path (, path)* 68 | path ::= step-initial step-subsequent* 69 | step-initial ::= axis? match result? filter? 70 | step-subsequent ::= axis match result? filter? 71 | 72 | The search axis can be either... 73 | 74 | - `/` for direct child nodes, or 75 | - `//` for any descendant nodes, or 76 | - `./` for current node plus direct child nodes, or 77 | - `.//` for current node plus any descendant nodes, or 78 | - `-/` for direct left sibling node, or 79 | - `-//` for any left sibling nodes, or 80 | - `+/` for direct right sibling node, or 81 | - `+//` for any right sibling nodes, or 82 | - `~/` for direct left and right sibling nodes, or 83 | - `~//` for all left and right sibling nodes, or 84 | - `../` for direct parent node, or 85 | - `..//` for any parent nodes, or 86 | - `//` for any following nodes. 88 | 89 | As an illustrating example: given an AST of the following particular nodes, ... 90 | 91 | A 92 | | 93 | +-+-+-+-+ 94 | / / | \ \ 95 | B C D E F 96 | | 97 | +--+--+ 98 | / | \ 99 | G H I 100 | | 101 | +-+-+ 102 | / \ 103 | J K 104 | 105 | ...the following queries and their result exist: 106 | 107 | Start Node | Query | Result Node(s) 108 | -----------|----------|--------------------------------- 109 | `D` | `/ *` | `G, H, I` 110 | `D` | `// *` | `G, H, J, K, I` 111 | `D` | `./ *` | `D, G, H, I` 112 | `D` | `.// *` | `D, G, H, J, K, I` 113 | `D` | `-/ *` | `C` 114 | `D` | `-// *` | `C, B` 115 | `D` | `+/ *` | `E` 116 | `D` | `+// *` | `E, F` 117 | `D` | `~/ *` | `C, E` 118 | `D` | `~// *` | `B, C, E, F` 119 | `H` | `../ *` | `D` 120 | `H` | `..// *` | `D, A` 121 | `H` | `// *` | `J, K, I, E, F` 123 | 124 | A search axis usually walks along the references between nodes (at least 125 | in case of ASTy based AST). But in case the underlying AST and its 126 | adapter uses typed references, you can optionally constrain the search 127 | axis to take only references matching the type `id` into account. 128 | 129 | axis ::= axis-direction axis-type? 130 | axis-direction ::= axis-child 131 | | axis-sibling-left 132 | | axis-sibling-right 133 | | axis-sibling 134 | | axis-parent 135 | | axis-preceding 136 | | axis-following 137 | axis-child ::= ("/" | "//" | "./" | ".//") 138 | axis-sibling-left ::= ("-/" | "-//") 139 | axis-sibling-right ::= ("+/" | "+//") 140 | axis-sibling ::= ("~/" | "~//") 141 | axis-parent ::= ("../" | "..//") 142 | axis-preceding ::= "//" 144 | axis-type ::= ":" (id | string) 145 | result ::= "!" 146 | match ::= id | string | "*" 147 | filter ::= "[" expr "]" 148 | 149 | The real power comes through the optional filter expression: it can be 150 | applied to each query step and it recursively(!) can contain sub-queries 151 | with the help of embedded query paths! 152 | An illustrating combined example is: 153 | 154 | // Foo / Bar [ / Baz [ @bar == 'baz' ] && / Quux ], // Foo2 155 | +---------------------------------------------------------+ query 156 | +------------------------------------------------+ +-----+ path 157 | +---------------------+ +-----+ path 158 | +----+ +-----------------------------------------+ +-----+ step 159 | ++ + + + ++ axis 160 | +-+ +-+ +-+ +--+ +--+ match 161 | +-----------------------------------+ filter 162 | +-------------------------------+ expr 163 | +---------------+ filter 164 | +----------+ expr 165 | 166 | The result of a query is always all nodes which match against the last 167 | query step of any path (in case of no result marker on any step in the 168 | path) or all nodes of matched steps with a result marker. The queries in 169 | filter expressions just lead to a boolean decision for the filter, but 170 | never cause any resulting nodes theirself. 171 | 172 | An expression can be either a ternary/binary conditional expression, 173 | logical expression, bitwise expression, relational expression, 174 | arithmethical expression, functional call, attribute reference, query 175 | parameter, literal value, parenthesis expression or path of a sub-query. 176 | 177 | expr ::= conditional 178 | | logical 179 | | bitwise 180 | | relational 181 | | arithmentical 182 | | function-call 183 | | attribute-ref 184 | | query-parameter 185 | | literal 186 | | parenthesis 187 | | sub-query 188 | conditional ::= expr "?" expr ":" expr 189 | | expr "?:" expr 190 | logical ::= expr ("&&" | "||") expr 191 | | "!" expr 192 | bitwise ::= expr ("&" | "|" | "<<" | ">>") expr 193 | | "~" expr 194 | relational ::= expr ("==" | "!=" | "<=" | ">=" | "<" | ">" | "=~" | "!~") expr 195 | arithmethical ::= expr ("+" | "-" | "*" | "/" | "%" | "**") expr 196 | function-call ::= id "(" (expr ("," expr)*)? ")" 197 | attribute-ref ::= "@" (id | string) 198 | query-parameter ::= "{" id "}" 199 | id ::= /[a-zA-Z_][a-zA-Z0-9_-]*/ 200 | literal ::= string | regexp | number | value 201 | string ::= /"(\\"|.)*"/ | /'(\\'|.)*'/ 202 | regexp ::= /`(\\`|.)*`/ 203 | number ::= /\d+(\.\d+)?$/ 204 | value ::= "true" | "false" | "null" | "NaN" | "undefined" 205 | parenthesis ::= "(" expr ")" 206 | sub-query ::= path // <-- ESSENTIAL RECURSION !! 207 | 208 | Notice that the function call parameters can be full expressions theirself, 209 | including (through the recursion over `sub-query` above) full query paths. 210 | The available pre-defined standard functions are: 211 | 212 | - `type(): String`:
213 | Return type of current node. 214 | Example: `type() == "foo"` 215 | 216 | - `attrs(sep: String): String`:
217 | Return the `sep`-separated concatenation of all attribute names of 218 | current node. The `sep` string is alway also prepended and appended 219 | for easier comparison of the result string. 220 | Example: `attr(",") == ",foo,bar,"` 221 | 222 | - `depth(): Number`:
223 | Return depth in AST of current node (counting from 1 for the root node). 224 | Example: `depth() <= 3` 225 | 226 | - `pos(): Number`:
227 | Return position of current node among sibling (counting from 1 for the first sibling). 228 | Example: `pos() == 2` 229 | 230 | - `nth(pos: Number): Boolean`:
231 | Check whether position of current node among sibling is `pos` (counting from 1 for 232 | the first sibling). Negative values for `pos` count from the last sibling backward, 233 | i.e., `-1` is the last sibling. 234 | Example: `nth(3)` 235 | 236 | - `first(): Boolean`:
237 | Shorthand for `nth(1)`. 238 | 239 | - `last(): Boolean`:
240 | Shorthand for `nth(-1)`. 241 | 242 | - `count(array: Object[]): Number`:
243 | Return the number of elements in `array`. 244 | The `array` usually is either an externally passed-in parameter or a sub-query. 245 | Example: `count({nodes}) <= count(// *)` 246 | 247 | - `below(node: Node): Boolean`:
248 | Checks whether current node is somewhere below `node`, i.e., 249 | whether current node is a child or descendant of `node`. Usually, 250 | this makes sense only if `node` is an externally passed-in parameter. 251 | Example: `below({node})`. 252 | 253 | - `follows(node: Node): Boolean`:
254 | Checks whether current node is following `node`, i.e., 255 | whether current node comes after `node` in a standard 256 | depth-first tree visit (where parents are visited before childs). 257 | Usually, this makes sense only if `node` is an externally passed-in parameter. 258 | Example: `follows({node})`. 259 | 260 | - `in(nodes: Node[]): Number`:
261 | Checks whether current node is in `nodes`. 262 | Usually, `nodes` is either an externally passed-in parameter or a sub-query. 263 | Example: `in({nodes})`. 264 | 265 | - `substr(str: String, pos: Number, len: Number): String`:
266 | Returns the sub-string of `str`, starting at `pos` with length `len`. 267 | Negative values for `pos` count from the end of the string, 268 | i.e., `-1` is the last character. 269 | Example: `substr(@foo, 0, 1) == "A"` 270 | 271 | - `index(str: String, sub: String, pos: Number): Number`:
272 | Returns the index position of sub-string `sub` in string `str`, starting at `pos`. 273 | Example: `indexof(@foo, "bar", 0) >= 0` 274 | 275 | - `trim(str: String): String`:
276 | Returns the string `str` with whitespaces removed from begin and end. 277 | Example: `trim(@foo) == "bar"` 278 | 279 | - `lc(str: String): String`:
280 | Returns the lower-case variant of `str`. 281 | Example: `lc(@foo) == "bar"` 282 | 283 | - `uc(str: String): String`:
284 | Returns the upper-case variant of `str`. 285 | Example: `uc(@foo) == "BAR"` 286 | 287 | Application Programming Interface (API) 288 | --------------------------------------- 289 | 290 | The ASTq API, here assumed to be exposed through the variable `ASTQ`, 291 | provides the following methods (in a notation somewhat resembling 292 | TypeScript type definitions): 293 | 294 | ### ASTQ API 295 | 296 | - `new ASTQ(): ASTQ`:
297 | Create a new ASTQ instance. 298 | 299 | - `ASTQ#adapter(adapter: (ASTQAdapter | ASTQAdapter[]), force: Boolean): ASTQ`:
300 | Register one or more custom tree access adapter(s) to support arbitrary AST-style 301 | data structures. The `ASTQAdapter` has to conform to a particular 302 | duck-typed interface. See below for more information. 303 | By default ASTq has built-in adapters for ASTy, XML DOM, Parse5, Cheerio, UniST, JSON and Mozilla AST. 304 | All those "taste" the node passed to `ASTQ#query` and hence are auto-selected. 305 | Calling `adapter()` causes these to be replaced with a single custom adapter. 306 | Its "tasting" can be disabled with option `force` set to `true`. 307 | The `ASTQ#adapter` teturns the API itself. 308 | 309 | /* the built-in implementation for supporting ASTy */ 310 | astq.adapter({ 311 | taste: function (node) { return (typeof node === "object" && node.ASTy) }, 312 | getParentNode: function (node, type) { return node.parent() }, 313 | getChildNodes: function (node, type) { return node.childs() }, 314 | getNodeType: function (node) { return node.type() }, 315 | getNodeAttrNames: function (node) { return node.attrs() }, 316 | getNodeAttrValue: function (node, attr) { return node.get(attr) } 317 | }) 318 | 319 | - `ASTQ#version(): { major: Number, minor: Number, micro: Number, date: Number }`:
320 | Return the current ASTq library version details. 321 | 322 | - `ASTQ#func(name: String, func: (adapter: Adapter, node: Object, [...]) => Any): ASTQ`:
323 | Register function named `name` by providing the callback `func` which has 324 | to return an arbitrary value and optionally can access the current `node` with 325 | the help of the selected `adapter`. Returns the API itself. 326 | 327 | /* the built-in implementation for "depth" */ 328 | astq.func("depth", function (adapter, node) => { 329 | var depth = 1 330 | while ((node = adapter.getParentNode(node)) !== null) 331 | depth++ 332 | return depth 333 | }) 334 | 335 | - `ASTQ#cache(num: Number): ASTQ`:
336 | Set the upper limit for the internal query cache to `num`, i.e., 337 | up to `num` ASTs of parsed queries will be cached. Set `num` to 338 | `0` to disable the cache at all. Returns the API itself. 339 | 340 | - `ASTQ#compile(selector: String, trace?: Boolean): ASTQQuery { 341 | Compile `selector` DSL into an internal query object for subsequent 342 | processing by `ASTQ#execute`. 343 | If `trace` is `true` the compiling is dumped to the console. 344 | Returns the query object. 345 | 346 | - `ASTQ#execute(node: Object, query: ASTQQuery, params?: Object, trace?: Boolean): Object[]`:
347 | Execute the previously compiled `query` (see `compile` above) at `node`. 348 | The optional `params` object can provide parameters for the `{name}` query constructs. 349 | If `trace` is `true` the execution is dumped to the console. 350 | Returns an array of zero or more matching AST nodes. 351 | 352 | - `ASTQ#query(node: Object, selector: String, params?: Object, trace?: Boolean): Object[]`:
353 | Just the convenient combination of `compile` and `execute`: 354 | `execute(node, compile(selector, trace), params, trace)`. 355 | Use this as the standard query method except you need more control. 356 | The optional `params` object can provide parameters for the `{name}` query constructs. 357 | If `trace` is `true` the compiling and execution is dumped to the console. 358 | Returns an array of zero or more matching AST nodes. 359 | 360 | ### ASTQAdapter API 361 | 362 | For accessing arbitrary AST-style data structures, an adapter has to be 363 | provided. By default ASTq has adapters for use with ASTy, XML DOM, Parse5, Cheerio, UniST, JSON and 364 | Mozilla AST. The `ASTQAdapter` interface is: 365 | 366 | - `ASTQAdapter#taste(node: Object): Boolean`:
367 | Taste `node` to be sure this adapter is intended to handle it. 368 | 369 | - `ASTQAdapter#getParentNode(node: Object): Object`:
370 | Return parent node of `node`. In case the underyling 371 | data structure does not support traversing to parent nodes, 372 | throw an exception. 373 | 374 | - `ASTQAdapter#getChildNodes(node: Object): Object[]`:
375 | Return the list of all child nodes of `node`. 376 | 377 | - `ASTQAdapter#getNodeType(node: Object): String`:
378 | Return the type of `node`. 379 | 380 | - `ASTQAdapter#getNodeAttrNames(node: Object): String[]`:
381 | Return the list of all attribute names of `node`. 382 | 383 | - `ASTQAdapter#getNodeAttrValue(node: Object, attr: String): Any`:
384 | Return the value of attribute `attr` of `node`. 385 | 386 | Example 387 | ------- 388 | 389 | ``` 390 | $ cat sample.js 391 | const acorn = require("acorn") 392 | const ASTQ = require("astq") 393 | 394 | let source = ` 395 | class Foo { 396 | foo () { 397 | const bar = "quux" 398 | let baz = 42 399 | } 400 | } 401 | ` 402 | 403 | let ast = acorn.parse(source, { ecmaVersion: 6 }) 404 | 405 | let astq = new ASTQ() 406 | astq.adapter("mozast") 407 | astq.query(ast, ` 408 | // VariableDeclarator [ 409 | /:id Identifier [ @name ] 410 | && /:init Literal [ @value ] 411 | ] 412 | `).forEach(function (node) { 413 | console.log(`${node.id.name}: ${node.init.value}`) 414 | }) 415 | 416 | $ babel-node sample.js 417 | bar: quux 418 | baz: 42 419 | ``` 420 | 421 | Implementation Notice 422 | --------------------- 423 | 424 | Although ASTq is written in ECMAScript 2018, it is transpiled to older 425 | environments and this way runs in really all current (as of 2018) 426 | JavaScript environments, of course. 427 | 428 | Additionally, there are two transpilation results: first, there is a 429 | compressed `astq.browser.js` for Browser environments. Second, there is 430 | an uncompressed `astq.node.js` for Node.js environments. 431 | 432 | The Browser variant `astq.browser.js` has all external dependencies `asty`, 433 | `pegjs-otf`, `pegjs-util`, and `cache-lru` directly embedded. The 434 | Node.js variant `astq.node.js` still requires the external dependencies 435 | `asty`, `pegjs-otf`, `pegjs-util`, and `cache-lru`. 436 | 437 | License 438 | ------- 439 | 440 | Copyright © 2014-2024 Dr. Ralf S. Engelschall (http://engelschall.com/) 441 | 442 | Permission is hereby granted, free of charge, to any person obtaining 443 | a copy of this software and associated documentation files (the 444 | "Software"), to deal in the Software without restriction, including 445 | without limitation the rights to use, copy, modify, merge, publish, 446 | distribute, sublicense, and/or sell copies of the Software, and to 447 | permit persons to whom the Software is furnished to do so, subject to 448 | the following conditions: 449 | 450 | The above copyright notice and this permission notice shall be included 451 | in all copies or substantial portions of the Software. 452 | 453 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 454 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 455 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 456 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 457 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 458 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 459 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 460 | 461 | -------------------------------------------------------------------------------- /VERSION.yml: -------------------------------------------------------------------------------- 1 | ## 2 | ## ASTq -- Abstract Syntax Tree (AST) Query Engine 3 | ## Copyright (c) 2014-2024 Dr. Ralf S. Engelschall 4 | ## 5 | ## Permission is hereby granted, free of charge, to any person obtaining 6 | ## a copy of this software and associated documentation files (the 7 | ## "Software"), to deal in the Software without restriction, including 8 | ## without limitation the rights to use, copy, modify, merge, publish, 9 | ## distribute, sublicense, and/or sell copies of the Software, and to 10 | ## permit persons to whom the Software is furnished to do so, subject to 11 | ## the following conditions: 12 | ## 13 | ## The above copyright notice and this permission notice shall be included 14 | ## in all copies or substantial portions of the Software. 15 | ## 16 | ## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ## EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ## MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ## IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ## CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ## TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ## SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | ## 24 | 25 | major: 2 26 | minor: 8 27 | micro: 1 28 | date: 20240308 29 | 30 | -------------------------------------------------------------------------------- /eslint.yaml: -------------------------------------------------------------------------------- 1 | ## 2 | ## ASTy -- Abstract Syntax Tree (AST) Data Structure 3 | ## Copyright (c) 2014-2024 Dr. Ralf S. Engelschall 4 | ## 5 | ## Permission is hereby granted, free of charge, to any person obtaining 6 | ## a copy of this software and associated documentation files (the 7 | ## "Software"), to deal in the Software without restriction, including 8 | ## without limitation the rights to use, copy, modify, merge, publish, 9 | ## distribute, sublicense, and/or sell copies of the Software, and to 10 | ## permit persons to whom the Software is furnished to do so, subject to 11 | ## the following conditions: 12 | ## 13 | ## The above copyright notice and this permission notice shall be included 14 | ## in all copies or substantial portions of the Software. 15 | ## 16 | ## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ## EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ## MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ## IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ## CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ## TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ## SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | ## 24 | 25 | --- 26 | 27 | extends: 28 | - eslint:recommended 29 | - eslint-config-standard 30 | 31 | parserOptions: 32 | ecmaVersion: 12 33 | sourceType: module 34 | ecmaFeatures: 35 | jsx: false 36 | 37 | env: 38 | browser: true 39 | node: true 40 | mocha: true 41 | commonjs: true 42 | worker: true 43 | serviceworker: true 44 | 45 | globals: 46 | process: true 47 | 48 | rules: 49 | # modified rules 50 | indent: [ "error", 4, { "SwitchCase": 1 } ] 51 | linebreak-style: [ "error", "unix" ] 52 | semi: [ "error", "never" ] 53 | operator-linebreak: [ "error", "after", { "overrides": { "&&": "before", "||": "before", ":": "before" } } ] 54 | brace-style: [ "error", "stroustrup", { "allowSingleLine": true } ] 55 | quotes: [ "error", "double" ] 56 | 57 | # disabled rules 58 | no-multi-spaces: off 59 | no-multiple-empty-lines: off 60 | key-spacing: off 61 | object-property-newline: off 62 | curly: off 63 | space-in-parens: off 64 | array-bracket-spacing: off 65 | prefer-const: off 66 | lines-between-class-members: off 67 | quote-props: off 68 | multiline-ternary: off 69 | 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astq", 3 | "version": "2.8.1", 4 | "description": "Abstract Syntax Tree (AST) Query Engine", 5 | "keywords": [ "abstract", "syntax", "tree", "query", "engine", "adaptable" ], 6 | "license": "MIT", 7 | "browser": "lib/astq.browser.js", 8 | "main": "lib/astq.node.js", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/rse/astq.git" 12 | }, 13 | "author": { 14 | "name": "Dr. Ralf S. Engelschall", 15 | "email": "rse@engelschall.com", 16 | "url": "http://engelschall.com" 17 | }, 18 | "homepage": "https://github.com/rse/astq", 19 | "bugs": "https://github.com/rse/astq/issues", 20 | "dependencies": { 21 | "peggy": "4.0.2", 22 | "pegjs-otf": "2.0.2", 23 | "pegjs-util": "2.0.1", 24 | "asty": "1.8.21", 25 | "cache-lru": "1.2.0" 26 | }, 27 | "devDependencies": { 28 | "grunt": "1.6.1", 29 | "grunt-cli": "1.4.3", 30 | "grunt-contrib-clean": "2.0.1", 31 | "grunt-browserify": "6.0.0", 32 | "grunt-mocha-test": "0.13.3", 33 | "grunt-eslint": "24.3.0", 34 | "@babel/core": "7.24.0", 35 | "eslint": "8.57.0", 36 | "eslint-config-standard": "17.1.0", 37 | "eslint-plugin-promise": "6.1.1", 38 | "eslint-plugin-import": "2.29.1", 39 | "eslint-plugin-node": "11.1.0", 40 | "mocha": "10.3.0", 41 | "chai": "4.4.1", 42 | "babelify": "10.0.0", 43 | "@babel/preset-env": "7.24.0", 44 | "uglifyify": "5.0.2", 45 | "browserify-header": "1.1.0", 46 | "browserify-derequire": "1.1.1", 47 | "browserify-replace": "1.1.0" 48 | }, 49 | "engines": { 50 | "node": ">=12.0.0" 51 | }, 52 | "scripts": { 53 | "prepublishOnly": "grunt default", 54 | "build": "grunt default" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /smp/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /smp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astq-sample", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "acorn": "8.11.3", 6 | "parse5": "7.1.2", 7 | "xmldom": "0.6.0", 8 | "less": "4.2.0", 9 | "cheerio": "1.0.0-rc.12", 10 | "unified": "9.2.2", 11 | "remark-parse": "9.0.0" 12 | }, 13 | "scripts": { 14 | "start": "node sample.js" 15 | }, 16 | "engines": { 17 | "node": ">=12.0.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /smp/sample.js: -------------------------------------------------------------------------------- 1 | 2 | const ASTQ = require("..") 3 | 4 | /* query ECMAScript source-code */ 5 | ;(() => { 6 | let source = ` 7 | class Foo { 8 | foo () { 9 | const bar = "quux" 10 | let baz = 42 11 | } 12 | } 13 | ` 14 | const acorn = require("acorn") 15 | let ast = acorn.parse(source, { ecmaVersion: 6 }) 16 | let astq = new ASTQ() 17 | astq.adapter("mozast") 18 | astq.query(ast, ` 19 | /* query all variable declaration */ 20 | // VariableDeclarator [ 21 | /:id Identifier [ @name ] 22 | && /:init Literal [ @value ] 23 | ] 24 | `).forEach((node) => { 25 | console.log(`FOUND MOZAST: variable: ${node.id.name}, value: ${node.init.value}`) 26 | }) 27 | })(); 28 | 29 | /* query HTML source-code */ 30 | (() => { 31 | let source = ` 32 | 33 | 34 | Foo 35 | 36 | 37 |

Bar Baz

38 | Sample 39 |

Bar

40 | Sample 41 |

Quux

42 | Sample 43 | 44 | 45 | ` 46 | const parse5 = require("parse5") 47 | let ast = parse5.parse(source) 48 | let astq = new ASTQ() 49 | astq.adapter("parse5") 50 | astq.query(ast, ` 51 | /* query all headlines */ 52 | // * [ 53 | type() =~ \`^h[1-9]$\` 54 | && / "#text" 55 | ] 56 | `).forEach((node) => { 57 | let tag = node.nodeName 58 | let value = astq.query(node, `// "#text"`).map((node) => node.value).join("") 59 | console.log(`FOUND PARSE5: tag: ${tag}, value: ${value}`) 60 | }) 61 | })(); 62 | 63 | /* query XML source-code */ 64 | (() => { 65 | let source = ` 66 | 67 | Foo 68 | Bar 69 | Quux 70 | 71 | ` 72 | const xmldom = require("xmldom") 73 | var DOMParser = xmldom.DOMParser 74 | var ast = new DOMParser().parseFromString(source) 75 | let astq = new ASTQ() 76 | astq.adapter("xmldom", true) 77 | astq.query(ast, ` 78 | // * [ 79 | @enabled == "true" 80 | ] 81 | `).forEach((node) => { 82 | console.log(`FOUND XMLDOM: ${node.nodeName}`) 83 | }) 84 | })(); 85 | 86 | /* query JSON source-code */ 87 | (() => { 88 | let ast = { 89 | config: { 90 | foo: { enabled: true, body: "Foo" }, 91 | bar: { body: "Bar" }, 92 | quux: { enabled: true, body: "Quux" } 93 | } 94 | } 95 | let astq = new ASTQ() 96 | astq.adapter("json", true) 97 | astq.query(ast, ` 98 | // * /:foo * [ 99 | @enabled == true 100 | ] 101 | `).forEach((node) => { 102 | console.log(`FOUND JSON: ${JSON.stringify(node)}`) 103 | }) 104 | })(); 105 | 106 | /* query HTML source-code */ 107 | (() => { 108 | let source = ` 109 | 110 | 111 | Foo 112 | 113 | 114 |

Bar Baz

115 | Sample 116 |

Bar

117 | Sample 118 |

Quux

119 | Sample 120 | 121 | 122 | ` 123 | const cheerio = require("cheerio") 124 | let $ = cheerio.load(source) 125 | let ast = $.root()[0] 126 | let astq = new ASTQ() 127 | astq.adapter("cheerio") 128 | astq.query(ast, ` 129 | /* query all headlines */ 130 | // * [ 131 | type() =~ \`^h[1-9]$\` 132 | && // "#text" 133 | ] 134 | `).forEach((node) => { 135 | let tag = node.tagName 136 | let value = astq.query(node, `// "#text"`).map((node) => node.nodeValue).join("") 137 | console.log(`FOUND CHEERIO: tag: ${tag}, value: ${value}`) 138 | }) 139 | })(); 140 | 141 | /* query UniST/MDAST */ 142 | (() => { 143 | let source = 144 | "# Foo\n" + 145 | "Foo\n" + 146 | "# Bar\n" + 147 | "Bar\n" 148 | const unified = require("unified") 149 | const markdown = require("remark-parse") 150 | const ast = unified().use(markdown).parse(source) 151 | const astq = new ASTQ() 152 | astq.adapter("unist") 153 | astq.query(ast, ` 154 | /* query all text elements */ 155 | // text 156 | `).forEach((node) => { 157 | console.log(`FOUND UniST: tag: ${node.type}, value: ${node.value}`) 158 | }) 159 | })(); 160 | 161 | -------------------------------------------------------------------------------- /src/astq-adapter-asty.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** ASTq -- Abstract Syntax Tree (AST) Query Engine 3 | ** Copyright (c) 2014-2024 Dr. Ralf S. Engelschall 4 | ** 5 | ** Permission is hereby granted, free of charge, to any person obtaining 6 | ** a copy of this software and associated documentation files (the 7 | ** "Software"), to deal in the Software without restriction, including 8 | ** without limitation the rights to use, copy, modify, merge, publish, 9 | ** distribute, sublicense, and/or sell copies of the Software, and to 10 | ** permit persons to whom the Software is furnished to do so, subject to 11 | ** the following conditions: 12 | ** 13 | ** The above copyright notice and this permission notice shall be included 14 | ** in all copies or substantial portions of the Software. 15 | ** 16 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | export default class ASTQAdapterASTY { 26 | static taste (node) { 27 | return (typeof node === "object" && node !== null && typeof node.ASTy === "boolean") 28 | } 29 | static getParentNode (node /*, type */) { 30 | return node.parent() 31 | } 32 | static getChildNodes (node /*, type */) { 33 | return node.childs() 34 | } 35 | static getNodeType (node) { 36 | return node.type() 37 | } 38 | static getNodeAttrNames (node) { 39 | return node.attrs() 40 | } 41 | static getNodeAttrValue (node, attr) { 42 | return node.get(attr) 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /src/astq-adapter-cheerio.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** ASTq -- Abstract Syntax Tree (AST) Query Engine 3 | ** Copyright (c) 2014-2024 Dr. Ralf S. Engelschall 4 | ** 5 | ** Permission is hereby granted, free of charge, to any person obtaining 6 | ** a copy of this software and associated documentation files (the 7 | ** "Software"), to deal in the Software without restriction, including 8 | ** without limitation the rights to use, copy, modify, merge, publish, 9 | ** distribute, sublicense, and/or sell copies of the Software, and to 10 | ** permit persons to whom the Software is furnished to do so, subject to 11 | ** the following conditions: 12 | ** 13 | ** The above copyright notice and this permission notice shall be included 14 | ** in all copies or substantial portions of the Software. 15 | ** 16 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | export default class ASTQAdapterCheerio { 26 | static taste (node) { 27 | /* global Document: true */ 28 | /* global Element: true */ 29 | return ( 30 | typeof node === "object" 31 | && node !== null 32 | && (( 33 | !(typeof Element === "object" && node instanceof Element) 34 | && typeof node.tagName === "string" 35 | && node.tagName !== "" 36 | ) || ( 37 | !(typeof Document === "object" && node instanceof Document) 38 | && typeof node.type === "string" 39 | && node.type === "root" 40 | )) 41 | ) 42 | } 43 | static getParentNode (node /*, type */) { 44 | return node.parentNode 45 | } 46 | static getChildNodes (node /*, type */) { 47 | return ((typeof node.childNodes === "object" 48 | && node.childNodes instanceof Array) ? 49 | node.childNodes : []) 50 | } 51 | static getNodeType (node) { 52 | return node.tagName || `#${node.type || "unknown"}` 53 | } 54 | static getNodeAttrNames (node) { 55 | let attrs = [ "value" ] 56 | if (typeof node.attribs === "object") 57 | attrs = attrs.concat(Object.keys(node.attribs)) 58 | return attrs 59 | } 60 | static getNodeAttrValue (node, attr) { 61 | let value 62 | if (attr === "value") 63 | value = node.nodeValue 64 | else if (typeof node.attribs === "object") 65 | value = node.attribs[attr] 66 | return value 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /src/astq-adapter-graphql.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** ASTq -- Abstract Syntax Tree (AST) Query Engine 3 | ** Copyright (c) 2014-2024 Dr. Ralf S. Engelschall 4 | ** 5 | ** Permission is hereby granted, free of charge, to any person obtaining 6 | ** a copy of this software and associated documentation files (the 7 | ** "Software"), to deal in the Software without restriction, including 8 | ** without limitation the rights to use, copy, modify, merge, publish, 9 | ** distribute, sublicense, and/or sell copies of the Software, and to 10 | ** permit persons to whom the Software is furnished to do so, subject to 11 | ** the following conditions: 12 | ** 13 | ** The above copyright notice and this permission notice shall be included 14 | ** in all copies or substantial portions of the Software. 15 | ** 16 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | /* See also: https://graphql.org/graphql-js/type/#graphqlobjecttype */ 26 | 27 | export default class ASTQAdapterGraphQL { 28 | static taste (node) { 29 | return (typeof node === "object" 30 | && node !== null 31 | && typeof node.kind === "string" 32 | && node.kind !== "") 33 | } 34 | static getParentNode (node, type) { 35 | throw new Error("GraphQL AST does not support parent node traversal") 36 | } 37 | static getChildNodes (node, type) { 38 | let childs = [] 39 | let checkField = (node, field) => { 40 | if ( Object.prototype.hasOwnProperty.call(node, field) 41 | && this.taste(node[field])) 42 | childs.push(node[field]) 43 | else if ( Object.prototype.hasOwnProperty.call(node, field) 44 | && typeof node[field] === "object" 45 | && node[field] instanceof Array) { 46 | node[field].forEach((node) => { 47 | if (this.taste(node)) 48 | childs.push(node) 49 | }) 50 | } 51 | } 52 | if (type === "*") { 53 | for (let field in node) 54 | checkField(node, field) 55 | } 56 | else { 57 | if (typeof node[type] !== "undefined") 58 | checkField(node, type) 59 | } 60 | return childs 61 | } 62 | static getNodeType (node) { 63 | return node.kind 64 | } 65 | static getNodeAttrNames (node) { 66 | let names = [] 67 | for (let field in node) 68 | if ( Object.prototype.hasOwnProperty.call(node, field) 69 | && typeof node[field] !== "object" 70 | && field !== "kind" 71 | && field !== "loc") 72 | names.push(field) 73 | return names 74 | } 75 | static getNodeAttrValue (node, attr) { 76 | if ( Object.prototype.hasOwnProperty.call(node, attr) 77 | && typeof node[attr] !== "object" 78 | && attr !== "kind" 79 | && attr !== "loc") 80 | return node[attr] 81 | else 82 | return undefined 83 | } 84 | } 85 | 86 | -------------------------------------------------------------------------------- /src/astq-adapter-json.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** ASTq -- Abstract Syntax Tree (AST) Query Engine 3 | ** Copyright (c) 2014-2024 Dr. Ralf S. Engelschall 4 | ** 5 | ** Permission is hereby granted, free of charge, to any person obtaining 6 | ** a copy of this software and associated documentation files (the 7 | ** "Software"), to deal in the Software without restriction, including 8 | ** without limitation the rights to use, copy, modify, merge, publish, 9 | ** distribute, sublicense, and/or sell copies of the Software, and to 10 | ** permit persons to whom the Software is furnished to do so, subject to 11 | ** the following conditions: 12 | ** 13 | ** The above copyright notice and this permission notice shall be included 14 | ** in all copies or substantial portions of the Software. 15 | ** 16 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | export default class ASTQAdapterJSON { 26 | static taste (node) { 27 | return (typeof node === "object" && node !== null) 28 | } 29 | static getParentNode (node, type) { 30 | throw new Error("JSON does not support parent node traversal") 31 | } 32 | static getChildNodes (node, type) { 33 | let childs = [] 34 | let checkField = (node, field) => { 35 | if ( Object.prototype.hasOwnProperty.call(node, field) 36 | && this.taste(node[field])) 37 | childs.push(node[field]) 38 | else if ( Object.prototype.hasOwnProperty.call(node, field) 39 | && typeof node[field] === "object" 40 | && node[field] instanceof Array) { 41 | node[field].forEach((node) => { 42 | if (this.taste(node)) 43 | childs.push(node) 44 | }) 45 | } 46 | } 47 | if (type === "*") { 48 | for (let field in node) 49 | checkField(node, field) 50 | } 51 | else { 52 | if (typeof node[type] !== "undefined") 53 | checkField(node, type) 54 | } 55 | return childs 56 | } 57 | static getNodeType (node) { 58 | if (node === null) 59 | return "Null" 60 | else if (node instanceof Boolean) 61 | return "Boolean" 62 | else if (node instanceof Number) 63 | return "Number" 64 | else if (node instanceof String) 65 | return "String" 66 | else if (typeof node === "object") { 67 | if (node instanceof Array) 68 | return "Array" 69 | else if (typeof node.constructor === "function" && typeof node.constructor.name === "string") 70 | return node.constructor.name 71 | else 72 | return "Object" 73 | } 74 | else 75 | return "Unknown" 76 | } 77 | static getNodeAttrNames (node) { 78 | let names = [] 79 | for (let field in node) 80 | if ( Object.prototype.hasOwnProperty.call(node, field) 81 | && typeof node[field] !== "object") 82 | names.push(field) 83 | return names 84 | } 85 | static getNodeAttrValue (node, attr) { 86 | if ( Object.prototype.hasOwnProperty.call(node, attr) 87 | && typeof node[attr] !== "object") 88 | return node[attr] 89 | else 90 | return undefined 91 | } 92 | } 93 | 94 | -------------------------------------------------------------------------------- /src/astq-adapter-mozast.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** ASTq -- Abstract Syntax Tree (AST) Query Engine 3 | ** Copyright (c) 2014-2024 Dr. Ralf S. Engelschall 4 | ** 5 | ** Permission is hereby granted, free of charge, to any person obtaining 6 | ** a copy of this software and associated documentation files (the 7 | ** "Software"), to deal in the Software without restriction, including 8 | ** without limitation the rights to use, copy, modify, merge, publish, 9 | ** distribute, sublicense, and/or sell copies of the Software, and to 10 | ** permit persons to whom the Software is furnished to do so, subject to 11 | ** the following conditions: 12 | ** 13 | ** The above copyright notice and this permission notice shall be included 14 | ** in all copies or substantial portions of the Software. 15 | ** 16 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | /* See also: https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/Parser_API */ 26 | 27 | export default class ASTQAdapterMozAST { 28 | static taste (node) { 29 | return (typeof node === "object" 30 | && node !== null 31 | && typeof node.type === "string" 32 | && node.type !== "") 33 | } 34 | static getParentNode (node, type) { 35 | if (type !== "*" && type !== "parent") 36 | throw new Error("no such axis named \"" + type + "\" for walking to parent nodes") 37 | if (typeof node.parent !== "undefined") 38 | return node.parent 39 | else 40 | throw new Error("Your Mozilla SpiderMonkey AST does not support parent node traversal") 41 | } 42 | static getChildNodes (node, type) { 43 | let childs = [] 44 | let checkField = (node, field) => { 45 | if ( Object.prototype.hasOwnProperty.call(node, field) 46 | && this.taste(node[field])) 47 | childs.push(node[field]) 48 | else if ( Object.prototype.hasOwnProperty.call(node, field) 49 | && typeof node[field] === "object" 50 | && node[field] instanceof Array) { 51 | node[field].forEach((node) => { 52 | if (this.taste(node)) 53 | childs.push(node) 54 | }) 55 | } 56 | } 57 | if (type === "*") { 58 | for (let field in node) 59 | checkField(node, field) 60 | } 61 | else { 62 | if (typeof node[type] !== "undefined") 63 | checkField(node, type) 64 | } 65 | return childs 66 | } 67 | static getNodeType (node) { 68 | return node.type 69 | } 70 | static getNodeAttrNames (node) { 71 | let names = [] 72 | for (let field in node) 73 | if ( Object.prototype.hasOwnProperty.call(node, field) 74 | && typeof node[field] !== "object" 75 | && field !== "type" 76 | && field !== "loc") 77 | names.push(field) 78 | return names 79 | } 80 | static getNodeAttrValue (node, attr) { 81 | if ( Object.prototype.hasOwnProperty.call(node, attr) 82 | && typeof node[attr] !== "object" 83 | && attr !== "type" 84 | && attr !== "loc") 85 | return node[attr] 86 | else 87 | return undefined 88 | } 89 | } 90 | 91 | -------------------------------------------------------------------------------- /src/astq-adapter-parse5.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** ASTq -- Abstract Syntax Tree (AST) Query Engine 3 | ** Copyright (c) 2014-2024 Dr. Ralf S. Engelschall 4 | ** 5 | ** Permission is hereby granted, free of charge, to any person obtaining 6 | ** a copy of this software and associated documentation files (the 7 | ** "Software"), to deal in the Software without restriction, including 8 | ** without limitation the rights to use, copy, modify, merge, publish, 9 | ** distribute, sublicense, and/or sell copies of the Software, and to 10 | ** permit persons to whom the Software is furnished to do so, subject to 11 | ** the following conditions: 12 | ** 13 | ** The above copyright notice and this permission notice shall be included 14 | ** in all copies or substantial portions of the Software. 15 | ** 16 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | export default class ASTQAdapterParse5 { 26 | static taste (node) { 27 | /* global Node: true */ 28 | return (typeof node === "object" 29 | && node !== null 30 | && !(typeof Node === "object" && node instanceof Node) 31 | && typeof node.nodeName === "string" 32 | && node.nodeName !== "") 33 | } 34 | static getParentNode (node /*, type */) { 35 | return node.parentNode 36 | } 37 | static getChildNodes (node /*, type */) { 38 | return ((typeof node.childNodes === "object" 39 | && node.childNodes instanceof Array) ? 40 | node.childNodes : []) 41 | } 42 | static getNodeType (node) { 43 | return node.nodeName 44 | } 45 | static getNodeAttrNames (node) { 46 | let attrs = [ "value" ] 47 | if (typeof node.attrs === "object" && node.attrs instanceof Array) 48 | attrs = attrs.concat(node.attrs.map((n) => n.name)) 49 | return attrs 50 | } 51 | static getNodeAttrValue (node, attr) { 52 | let value 53 | if (attr === "value") 54 | value = node.value 55 | else if (typeof node.attrs === "object" && node.attrs instanceof Array) { 56 | let values = node.attrs 57 | .filter((n) => n.name === attr) 58 | .map((n) => n.value) 59 | if (values.length === 1) 60 | value = values[0] 61 | } 62 | return value 63 | } 64 | } 65 | 66 | -------------------------------------------------------------------------------- /src/astq-adapter-unist.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** ASTq -- Abstract Syntax Tree (AST) Query Engine 3 | ** Copyright (c) 2014-2024 Dr. Ralf S. Engelschall 4 | ** 5 | ** Permission is hereby granted, free of charge, to any person obtaining 6 | ** a copy of this software and associated documentation files (the 7 | ** "Software"), to deal in the Software without restriction, including 8 | ** without limitation the rights to use, copy, modify, merge, publish, 9 | ** distribute, sublicense, and/or sell copies of the Software, and to 10 | ** permit persons to whom the Software is furnished to do so, subject to 11 | ** the following conditions: 12 | ** 13 | ** The above copyright notice and this permission notice shall be included 14 | ** in all copies or substantial portions of the Software. 15 | ** 16 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | export default class ASTQAdapterUniST { 26 | static taste (node) { 27 | return (typeof node === "object" 28 | && node !== null 29 | && typeof node.type === "string" 30 | && node.type !== "") 31 | } 32 | static getParentNode (node /*, type */) { 33 | if (typeof node.parent === "object" && node.parent !== null) 34 | return node.parent 35 | else 36 | throw new Error("Your UniST AST does not support parent node traversal") 37 | } 38 | static getChildNodes (node /*, type */) { 39 | return ((typeof node.children === "object" 40 | && node.children instanceof Array) ? 41 | node.children : []) 42 | } 43 | static getNodeType (node) { 44 | return node.type 45 | } 46 | static getNodeAttrNames (node) { 47 | const attrs = [] 48 | for (const attr in node) 49 | if ( Object.prototype.hasOwnProperty.call(node, attr) 50 | && attr !== "type" 51 | && attr !== "data" 52 | && attr !== "position" 53 | && attr !== "children" 54 | && typeof node[attr] !== "object") 55 | attrs.push(attr) 56 | return attrs 57 | } 58 | static getNodeAttrValue (node, attr) { 59 | if ( Object.prototype.hasOwnProperty.call(node, attr) 60 | && attr !== "type" 61 | && attr !== "data" 62 | && attr !== "position" 63 | && attr !== "children" 64 | && typeof node[attr] !== "object") 65 | return node[attr] 66 | else 67 | return undefined 68 | } 69 | } 70 | 71 | -------------------------------------------------------------------------------- /src/astq-adapter-xmldom.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** ASTq -- Abstract Syntax Tree (AST) Query Engine 3 | ** Copyright (c) 2014-2024 Dr. Ralf S. Engelschall 4 | ** 5 | ** Permission is hereby granted, free of charge, to any person obtaining 6 | ** a copy of this software and associated documentation files (the 7 | ** "Software"), to deal in the Software without restriction, including 8 | ** without limitation the rights to use, copy, modify, merge, publish, 9 | ** distribute, sublicense, and/or sell copies of the Software, and to 10 | ** permit persons to whom the Software is furnished to do so, subject to 11 | ** the following conditions: 12 | ** 13 | ** The above copyright notice and this permission notice shall be included 14 | ** in all copies or substantial portions of the Software. 15 | ** 16 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | export default class ASTQAdapterXMLDOM { 26 | static taste (node) { 27 | /* global Node: true */ 28 | return (typeof Node === "object" 29 | && node !== null 30 | && node instanceof Node 31 | && typeof node === "object" 32 | && typeof node.nodeType === "number" 33 | && typeof node.nodeName === "string") 34 | } 35 | static getParentNode (node /*, type */) { 36 | return node.parentNode 37 | } 38 | static getChildNodes (node /*, type */) { 39 | return typeof node.childNodes === "object" && node.childNodes !== null && node.hasChildNodes() ? 40 | Array.prototype.slice.call(node.childNodes, 0) : [] 41 | } 42 | static getNodeType (node) { 43 | return typeof node.nodeName === "string" ? 44 | node.nodeName : "unknown" 45 | } 46 | static getNodeAttrNames (node) { 47 | return typeof node.attributes === "object" && node.attributes !== null && node.hasAttributes() ? 48 | Array.prototype.slice.call(node.attributes, 0).map((n) => n.nodeName) : [] 49 | } 50 | static getNodeAttrValue (node, attr) { 51 | return typeof node.attributes === "object" && node.attributes !== null && node.hasAttributes() ? 52 | node.getAttribute(attr) : undefined 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /src/astq-adapter.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** ASTq -- Abstract Syntax Tree (AST) Query Engine 3 | ** Copyright (c) 2014-2024 Dr. Ralf S. Engelschall 4 | ** 5 | ** Permission is hereby granted, free of charge, to any person obtaining 6 | ** a copy of this software and associated documentation files (the 7 | ** "Software"), to deal in the Software without restriction, including 8 | ** without limitation the rights to use, copy, modify, merge, publish, 9 | ** distribute, sublicense, and/or sell copies of the Software, and to 10 | ** permit persons to whom the Software is furnished to do so, subject to 11 | ** the following conditions: 12 | ** 13 | ** The above copyright notice and this permission notice shall be included 14 | ** in all copies or substantial portions of the Software. 15 | ** 16 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | export default class ASTQAdapter { 26 | constructor () { 27 | this._adapters = [] 28 | return this 29 | } 30 | register (adapter, force = false) { 31 | this._adapters.unshift({ adapter, force }) 32 | return this 33 | } 34 | unregister (adapter) { 35 | if (adapter === undefined) 36 | this._adapters = [] 37 | else { 38 | let adapters = [] 39 | for (let i = 0; i < this._adapters.length; i++) 40 | if (this._adapters[i].adapter !== adapter) 41 | adapters.push(this._adapters[i]) 42 | this._adapters = adapters 43 | } 44 | return this 45 | } 46 | select (node) { 47 | for (let i = 0; i < this._adapters.length; i++) 48 | if (this._adapters[i].force || this._adapters[i].adapter.taste(node)) 49 | return this._adapters[i].adapter 50 | return undefined 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /src/astq-funcs-std.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** ASTq -- Abstract Syntax Tree (AST) Query Engine 3 | ** Copyright (c) 2014-2024 Dr. Ralf S. Engelschall 4 | ** 5 | ** Permission is hereby granted, free of charge, to any person obtaining 6 | ** a copy of this software and associated documentation files (the 7 | ** "Software"), to deal in the Software without restriction, including 8 | ** without limitation the rights to use, copy, modify, merge, publish, 9 | ** distribute, sublicense, and/or sell copies of the Software, and to 10 | ** permit persons to whom the Software is furnished to do so, subject to 11 | ** the following conditions: 12 | ** 13 | ** The above copyright notice and this permission notice shall be included 14 | ** in all copies or substantial portions of the Software. 15 | ** 16 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | /* internal helper function: find position between siblings */ 26 | const pos = (A, T) => { 27 | let parent = A.getParentNode(T, "*") 28 | if (parent === null) 29 | return 1 30 | let pchilds = A.getChildNodes(parent, "*") 31 | for (let i = 0; i < pchilds.length; i++) 32 | if (pchilds[i] === T) 33 | return (i + 1) 34 | throw new Error("cannot find myself") 35 | } 36 | 37 | /* internal helper function: find parent nodes */ 38 | const parents = (A, T) => { 39 | let parents = [] 40 | while ((T = A.getParentNode(T, "*")) !== null) 41 | parents.push(T) 42 | return parents 43 | } 44 | 45 | /* the exported standard functions */ 46 | const stdfuncs = { 47 | /* type name of node */ 48 | "type": (A, T) => { 49 | return A.getNodeType(T) 50 | }, 51 | 52 | /* attribute names of node */ 53 | "attrs": (A, T, sep) => { 54 | if (sep === undefined) 55 | sep = " " 56 | /* eslint no-console: 0 */ 57 | return sep + A.getNodeAttrNames(T).join(sep) + sep 58 | }, 59 | 60 | /* depth of node in tree */ 61 | "depth": (A, T) => { 62 | let depth = 1 63 | let node = T 64 | while ((node = A.getParentNode(node, "*")) !== null) 65 | depth++ 66 | return depth 67 | }, 68 | 69 | /* return position of node between siblings */ 70 | "pos": (A, T) => { 71 | return pos(A, T) 72 | }, 73 | 74 | /* check position of node between siblings */ 75 | "nth": (A, T, num) => { 76 | num = parseInt(num, 10) 77 | let parent = A.getParentNode(T, "*") 78 | if (parent !== null) { 79 | let pchilds = A.getChildNodes(parent, "*") 80 | if (num < 0) 81 | num = pchilds.length - (num + 1) 82 | for (let i = 0; i < pchilds.length; i++) 83 | if (pchilds[i] === T) 84 | return ((i + 1) === num) 85 | return false 86 | } 87 | else if (num === 1) 88 | return true 89 | else 90 | return false 91 | }, 92 | 93 | /* check position of node to be first of siblings */ 94 | "first": (A, T) => { 95 | return stdfuncs.nth(A, T, 1) 96 | }, 97 | 98 | /* check position of node to be last of siblings */ 99 | "last": (A, T) => { 100 | return stdfuncs.nth(A, T, -1) 101 | }, 102 | 103 | /* count number of keys/elements/characters/etc */ 104 | "count": (A, T, val) => { 105 | if (typeof val === "object" && val instanceof Array) 106 | return val.length 107 | else if (typeof val === "object") 108 | return Object.keys(val).length 109 | else if (typeof val === "string") 110 | return val.length 111 | else 112 | return String(val).length 113 | }, 114 | 115 | /* check whether node is below another */ 116 | "below": (A, T, other) => { 117 | if (!A.taste(other)) 118 | throw new Error("invalid argument to function \"below\" (node expected)") 119 | let node = T 120 | while ((node = A.getParentNode(node, "*")) !== null) 121 | if (node === other) 122 | return true 123 | return false 124 | }, 125 | 126 | /* check whether node follows another */ 127 | "follows": (A, T, other) => { 128 | if (!A.taste(other)) 129 | throw new Error("invalid argument to function \"follows\" (node expected)") 130 | if (T === other) 131 | return false 132 | let pathOfT = [ T ].concat(parents(A, T)).reverse() 133 | let pathOfOther = [ other ].concat(parents(A, other)).reverse() 134 | let len = Math.min(pathOfT.length, pathOfOther.length) 135 | let i 136 | for (i = 0; i < len; i++) 137 | if (pathOfT[i] !== pathOfOther[i]) 138 | break 139 | if (i === 0) 140 | throw new Error("internal error: root nodes have to be same same") 141 | else if (i === len) { 142 | if (pathOfOther.length < pathOfT.length) 143 | return true 144 | else 145 | return false 146 | } 147 | else 148 | return pos(A, pathOfT[i]) > pos(A, pathOfOther[i]) 149 | }, 150 | 151 | /* check whether node is in a list of nodes */ 152 | "in": (A, T, val) => { 153 | if (!(typeof val === "object" && val instanceof Array)) 154 | throw new Error("invalid argument to function \"in\" (array expected)") 155 | for (let i = 0; i < val.length; i++) 156 | if (val[i] === T) 157 | return true 158 | return false 159 | }, 160 | 161 | /* retrieve a sub-string */ 162 | "substr": (A, T, str, pos, len) => { 163 | return String(str).substr(pos, len) 164 | }, 165 | 166 | /* retrieve index of a sub-string */ 167 | "index": (A, T, str, sub, from) => { 168 | return String(str).indexOf(sub, from) 169 | }, 170 | 171 | /* remove whitespaces at begin and end of string */ 172 | "trim": (A, T, str) => { 173 | return String(str).trim() 174 | }, 175 | 176 | /* convert string to lower-case */ 177 | "lc": (A, T, str) => { 178 | return String(str).toLowerCase() 179 | }, 180 | 181 | /* convert string to upper-case */ 182 | "uc": (A, T, str) => { 183 | return String(str).toUpperCase() 184 | } 185 | } 186 | 187 | export default stdfuncs 188 | 189 | -------------------------------------------------------------------------------- /src/astq-funcs.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** ASTq -- Abstract Syntax Tree (AST) Query Engine 3 | ** Copyright (c) 2014-2024 Dr. Ralf S. Engelschall 4 | ** 5 | ** Permission is hereby granted, free of charge, to any person obtaining 6 | ** a copy of this software and associated documentation files (the 7 | ** "Software"), to deal in the Software without restriction, including 8 | ** without limitation the rights to use, copy, modify, merge, publish, 9 | ** distribute, sublicense, and/or sell copies of the Software, and to 10 | ** permit persons to whom the Software is furnished to do so, subject to 11 | ** the following conditions: 12 | ** 13 | ** The above copyright notice and this permission notice shall be included 14 | ** in all copies or substantial portions of the Software. 15 | ** 16 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | export default class ASTQFuncs { 26 | constructor () { 27 | this._funcs = {} 28 | return this 29 | } 30 | register (name, func) { 31 | this._funcs[name] = func 32 | } 33 | run (name, args) { 34 | let func = this._funcs[name] 35 | if (typeof func !== "function") 36 | throw new Error("invalid function \"" + name + "\"") 37 | return func.apply(null, args) 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /src/astq-query-exec.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** ASTq -- Abstract Syntax Tree (AST) Query Engine 3 | ** Copyright (c) 2014-2024 Dr. Ralf S. Engelschall 4 | ** 5 | ** Permission is hereby granted, free of charge, to any person obtaining 6 | ** a copy of this software and associated documentation files (the 7 | ** "Software"), to deal in the Software without restriction, including 8 | ** without limitation the rights to use, copy, modify, merge, publish, 9 | ** distribute, sublicense, and/or sell copies of the Software, and to 10 | ** permit persons to whom the Software is furnished to do so, subject to 11 | ** the following conditions: 12 | ** 13 | ** The above copyright notice and this permission notice shall be included 14 | ** in all copies or substantial portions of the Software. 15 | ** 16 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | import util from "./astq-util.js" 26 | import ASTQQueryTrace from "./astq-query-trace.js" 27 | 28 | export default class ASTQQueryExec extends ASTQQueryTrace { 29 | constructor (adapter, params, funcs, trace) { 30 | super() 31 | this.adapter = adapter 32 | this.params = params 33 | this.funcs = funcs 34 | this.trace = trace 35 | } 36 | 37 | execQuery (Q, T) { 38 | this.traceBegin(Q, T) 39 | let output = [] 40 | 41 | /* iterate over all query paths */ 42 | Q.childs().forEach((Q) => { 43 | output = output.concat(this.execPath(Q, T)) 44 | }) 45 | 46 | this.traceEnd(Q, T, output) 47 | return output 48 | } 49 | 50 | execPath (Q, T) { 51 | this.traceBegin(Q, T) 52 | let nodes = [ T ] 53 | let result = [] 54 | let resultExplicit = false 55 | 56 | /* iterate over all steps of a query path */ 57 | Q.childs().forEach((Q) => { 58 | let output = [] 59 | nodes.forEach((T) => { 60 | output = output.concat(this.execStep(Q, T)) 61 | }) 62 | nodes = output 63 | if (Q.get("isResult")) { 64 | resultExplicit = true 65 | result = result.concat(nodes) 66 | } 67 | }) 68 | 69 | this.traceEnd(Q, T, nodes) 70 | return (resultExplicit ? result : nodes) 71 | } 72 | 73 | execStep (Q, T) { 74 | this.traceBegin(Q, T) 75 | 76 | /* determine (optional) axis, (mandatory) match and (optional) filter */ 77 | let childs = Q.childs() 78 | let axis = null 79 | let match = null 80 | let filter = null 81 | let i = 0 82 | if (i < childs.length && childs[i].type() === "Axis") 83 | axis = childs[i++] 84 | if (i < childs.length && childs[i].type() === "Match") 85 | match = childs[i++] 86 | if (i < childs.length && childs[i].type() === "Filter") 87 | filter = childs[i++] 88 | if (match === null) 89 | throw new Error("no matching part in query step") 90 | 91 | let nodes = [] 92 | 93 | /* helper function for matching and taking node */ 94 | let id = match.get("id") 95 | let matchAndTake = (T) => { 96 | let type = this.adapter.getNodeType(T) 97 | if (id === "*" || id === type) { 98 | let take = true 99 | if (filter !== null) 100 | if (!this.execFilter(filter, T)) 101 | take = false 102 | if (take) 103 | nodes.push(T) 104 | } 105 | } 106 | 107 | /* determine nodes along axis which potentially might match */ 108 | if (axis !== null) { 109 | let op = axis.get("op") 110 | let t = axis.get("type") 111 | if (op === "/") { 112 | /* direct child nodes */ 113 | this.adapter.getChildNodes(T, t).forEach((T) => matchAndTake(T)) 114 | } 115 | else if (op === "//") { 116 | /* transitive child nodes */ 117 | let walk = (T) => { 118 | matchAndTake(T) 119 | this.adapter.getChildNodes(T, t).forEach((T) => walk(T)) /* RECURSION */ 120 | } 121 | this.adapter.getChildNodes(T, t).forEach((T) => walk(T)) 122 | } 123 | else if (op === "./") { 124 | /* current node plus direct child nodes */ 125 | matchAndTake(T) 126 | this.adapter.getChildNodes(T, t).forEach((T) => matchAndTake(T)) 127 | } 128 | else if (op === ".//") { 129 | /* current node plus transitive child nodes */ 130 | matchAndTake(T) 131 | let walk = (T) => { 132 | matchAndTake(T) 133 | this.adapter.getChildNodes(T, t).forEach((T) => walk(T)) /* RECURSION */ 134 | } 135 | this.adapter.getChildNodes(T, t).forEach((T) => walk(T)) 136 | } 137 | else if (op === "-/") { 138 | /* direct left sibling */ 139 | let parent = this.adapter.getParentNode(T, "*") 140 | if (parent !== null) { 141 | let pchilds = this.adapter.getChildNodes(parent, t) 142 | let leftSibling = null 143 | for (let i = 0; i < pchilds.length; i++) { 144 | if (pchilds[i] === T) 145 | break 146 | leftSibling = pchilds[i] 147 | } 148 | if (leftSibling !== null) 149 | matchAndTake(leftSibling) 150 | } 151 | } 152 | else if (op === "-//") { 153 | /* transitive left siblings */ 154 | let parent = this.adapter.getParentNode(T, "*") 155 | if (parent !== null) { 156 | let pchilds = this.adapter.getChildNodes(parent, t) 157 | let i = 0 158 | for (; i < pchilds.length; i++) 159 | if (pchilds[i] === T) 160 | break 161 | for (i--; i >= 0; i--) 162 | matchAndTake(pchilds[i]) 163 | } 164 | } 165 | else if (op === "+/") { 166 | /* direct right sibling */ 167 | let parent = this.adapter.getParentNode(T, "*") 168 | if (parent !== null) { 169 | let pchilds = this.adapter.getChildNodes(parent, t) 170 | let i 171 | for (i = 0; i < pchilds.length; i++) 172 | if (pchilds[i] === T) 173 | break 174 | if (i < pchilds.length) 175 | matchAndTake(pchilds[++i]) 176 | } 177 | } 178 | else if (op === "+//") { 179 | /* transitive right siblings */ 180 | let parent = this.adapter.getParentNode(T, "*") 181 | if (parent !== null) { 182 | let pchilds = this.adapter.getChildNodes(parent, t) 183 | let i 184 | for (i = 0; i < pchilds.length; i++) 185 | if (pchilds[i] === T) 186 | break 187 | if (i < pchilds.length) 188 | for (i++; i < pchilds.length; i++) 189 | matchAndTake(pchilds[i]) 190 | } 191 | } 192 | else if (op === "~/") { 193 | /* direct left and right sibling */ 194 | let parent = this.adapter.getParentNode(T, "*") 195 | if (parent !== null) { 196 | let pchilds = this.adapter.getChildNodes(parent, t) 197 | let i 198 | for (i = 0; i < pchilds.length; i++) 199 | if (pchilds[i] === T) 200 | break 201 | if (i > 0) 202 | matchAndTake(pchilds[i - 1]) 203 | if (i < pchilds.length - 1) 204 | matchAndTake(pchilds[i + 1]) 205 | } 206 | } 207 | else if (op === "~//") { 208 | /* transitive left and right siblings */ 209 | let parent = this.adapter.getParentNode(T, "*") 210 | if (parent !== null) { 211 | let pchilds = this.adapter.getChildNodes(parent, t) 212 | for (let i = 0; i < pchilds.length; i++) 213 | if (pchilds[i] !== T) 214 | matchAndTake(pchilds[i]) 215 | } 216 | } 217 | else if (op === "../") { 218 | /* direct parent */ 219 | let parent = this.adapter.getParentNode(T, t) 220 | if (parent !== null) 221 | matchAndTake(parent) 222 | } 223 | else if (op === "..//") { 224 | /* transitive parents */ 225 | let node = T 226 | for (;;) { 227 | let parent = this.adapter.getParentNode(node, t) 228 | if (parent === null) 229 | break 230 | matchAndTake(parent) 231 | node = parent 232 | } 233 | } 234 | else if (op === " { 244 | if (T === ctx.sentinel) 245 | ctx.take = false 246 | if (ctx.take) 247 | matchAndTake(T) 248 | if (ctx.take) 249 | this.adapter.getChildNodes(T, t).forEach((T) => walk(T)) /* RECURSION */ 250 | } 251 | if (T !== ctx.sentinel) { 252 | matchAndTake(T) 253 | this.adapter.getChildNodes(T, t).forEach((T) => walk(T)) 254 | } 255 | nodes = nodes.reverse() 256 | } 257 | else if (op === ">//") { 258 | /* transitive following nodes */ 259 | let ctx = { sentinel: T, take: false } 260 | for (;;) { 261 | let parent = this.adapter.getParentNode(T, "*") 262 | if (parent === null) 263 | break 264 | T = parent 265 | } 266 | let walk = (T) => { 267 | if (ctx.take) 268 | matchAndTake(T) 269 | if (T === ctx.sentinel) 270 | ctx.take = true 271 | this.adapter.getChildNodes(T, t).forEach((T) => walk(T)) /* RECURSION */ 272 | } 273 | this.adapter.getChildNodes(T, t).forEach((T) => walk(T)) 274 | } 275 | } 276 | else 277 | /* current node */ 278 | matchAndTake(T) 279 | 280 | this.traceEnd(Q, T, nodes) 281 | return nodes 282 | } 283 | 284 | execFilter (Q, T) { 285 | this.traceBegin(Q, T) 286 | let expr = Q.childs()[0] 287 | let result = this.execExpr(expr, T) 288 | result = util.truthy(result) 289 | this.traceEnd(Q, T, result) 290 | return result 291 | } 292 | 293 | execExpr (Q, T) { 294 | switch (Q.type()) { 295 | case "ConditionalBinary": return this.execExprConditionalBinary(Q, T) 296 | case "ConditionalTernary": return this.execExprConditionalTernary(Q, T) 297 | case "Logical": return this.execExprLogical(Q, T) 298 | case "Bitwise": return this.execExprBitwise(Q, T) 299 | case "Relational": return this.execExprRelational(Q, T) 300 | case "Arithmetical": return this.execExprArithmetical(Q, T) 301 | case "Unary": return this.execExprUnary(Q, T) 302 | case "FuncCall": return this.execExprFuncCall(Q, T) 303 | case "Attribute": return this.execExprAttribute(Q, T) 304 | case "Param": return this.execExprParam(Q, T) 305 | case "LiteralString": return this.execExprLiteralString(Q, T) 306 | case "LiteralRegExp": return this.execExprLiteralRegExp(Q, T) 307 | case "LiteralNumber": return this.execExprLiteralNumber(Q, T) 308 | case "LiteralValue": return this.execExprLiteralValue(Q, T) 309 | case "Path": return this.execExprPath(Q, T) 310 | } 311 | } 312 | 313 | execExprConditionalBinary (Q, T) { 314 | this.traceBegin(Q, T) 315 | let result = this.execExpr(Q.childs()[0], T) 316 | if (!util.truthy(result)) 317 | result = this.execExpr(Q.childs()[1], T) 318 | this.traceEnd(Q, T, result) 319 | return result 320 | } 321 | 322 | execExprConditionalTernary (Q, T) { 323 | this.traceBegin(Q, T) 324 | let result = this.execExpr(Q.childs()[0], T) 325 | if (util.truthy(result)) 326 | result = this.execExpr(Q.childs()[1], T) 327 | else 328 | result = this.execExpr(Q.childs()[2], T) 329 | this.traceEnd(Q, T, result) 330 | return result 331 | } 332 | 333 | execExprLogical (Q, T) { 334 | this.traceBegin(Q, T) 335 | let result = false 336 | switch (Q.get("op")) { 337 | case "&&": 338 | result = util.truthy(this.execExpr(Q.childs()[0], T)) 339 | if (result) 340 | result = result && util.truthy(this.execExpr(Q.childs()[1], T)) 341 | break 342 | case "||": 343 | result = util.truthy(this.execExpr(Q.childs()[0], T)) 344 | if (!result) 345 | result = result || util.truthy(this.execExpr(Q.childs()[1], T)) 346 | break 347 | } 348 | this.traceEnd(Q, T, result) 349 | return result 350 | } 351 | 352 | execExprBitwise (Q, T) { 353 | this.traceBegin(Q, T) 354 | let v1 = util.coerce(this.execExpr(Q.childs()[0], T), "number") 355 | let v2 = util.coerce(this.execExpr(Q.childs()[1], T), "number") 356 | let result 357 | switch (Q.get("op")) { 358 | case "&": result = v1 & v2; break 359 | case "|": result = v1 | v2; break 360 | case "<<": result = v1 << v2; break 361 | case ">>": result = v1 >> v2; break 362 | } 363 | this.traceEnd(Q, T, result) 364 | return result 365 | } 366 | 367 | execExprRelational (Q, T) { 368 | this.traceBegin(Q, T) 369 | let v1 = this.execExpr(Q.childs()[0], T) 370 | let v2 = this.execExpr(Q.childs()[1], T) 371 | let result 372 | switch (Q.get("op")) { 373 | case "==": result = v1 === v2; break 374 | case "!=": result = v1 !== v2; break 375 | case "<=": result = util.coerce(v1, "number") <= util.coerce(v2, "number"); break 376 | case ">=": result = util.coerce(v1, "number") >= util.coerce(v2, "number"); break 377 | case "<": result = util.coerce(v1, "number") < util.coerce(v2, "number"); break 378 | case ">": result = util.coerce(v1, "number") > util.coerce(v2, "number"); break 379 | case "=~": result = util.coerce(v1, "string").match(util.coerce(v2, "regexp")) !== null; break 380 | case "!~": result = util.coerce(v1, "string").match(util.coerce(v2, "regexp")) === null; break 381 | } 382 | this.traceEnd(Q, T, result) 383 | return result 384 | } 385 | 386 | execExprArithmetical (Q, T) { 387 | this.traceBegin(Q, T) 388 | let v1 = this.execExpr(Q.childs()[0], T) 389 | let v2 = this.execExpr(Q.childs()[1], T) 390 | let result 391 | switch (Q.get("op")) { 392 | case "+": 393 | if (typeof v1 === "string") 394 | result = v1 + util.coerce(v2, "string") 395 | else 396 | result = util.coerce(v1, "number") + util.coerce(v2, "number") 397 | break 398 | case "-": result = util.coerce(v1, "number") + util.coerce(v2, "number"); break 399 | case "*": result = util.coerce(v1, "number") * util.coerce(v2, "number"); break 400 | case "/": result = util.coerce(v1, "number") / util.coerce(v2, "number"); break 401 | case "%": result = util.coerce(v1, "number") % util.coerce(v2, "number"); break 402 | case "**": result = Math.pow(util.coerce(v1, "number"), util.coerce(v2, "number")); break 403 | } 404 | this.traceEnd(Q, T, result) 405 | return result 406 | } 407 | 408 | execExprUnary (Q, T) { 409 | this.traceBegin(Q, T) 410 | let v = this.execExpr(Q.childs()[0], T) 411 | let result 412 | /* jscs: disable */ 413 | switch (Q.get("op")) { 414 | case "!": result = !util.coerce(v, "boolean"); break 415 | case "~": result = ~util.coerce(v, "number"); break 416 | } 417 | /* jscs: enable */ 418 | this.traceEnd(Q, T, result) 419 | return result 420 | } 421 | 422 | execExprFuncCall (Q, T) { 423 | this.traceBegin(Q, T) 424 | let id = Q.get("id") 425 | let args = [ this.adapter, T ] 426 | Q.childs().forEach((Q) => { 427 | args.push(this.execExpr(Q, T)) 428 | }) 429 | let result = this.funcs.run(id, args) 430 | this.traceEnd(Q, T, result) 431 | return result 432 | } 433 | 434 | execExprAttribute (Q, T) { 435 | this.traceBegin(Q, T) 436 | let id = Q.get("id") 437 | let result = this.adapter.getNodeAttrValue(T, id) 438 | this.traceEnd(Q, T, result) 439 | return result 440 | } 441 | 442 | execExprParam (Q, T) { 443 | this.traceBegin(Q, T) 444 | let id = Q.get("id") 445 | if (typeof this.params[id] === "undefined") 446 | throw new Error("invalid parameter \"" + id + "\"") 447 | let result = this.params[id] 448 | this.traceEnd(Q, T, result) 449 | return result 450 | } 451 | 452 | execExprLiteralString (Q, T) { 453 | this.traceBegin(Q, T) 454 | let result = Q.get("value") 455 | this.traceEnd(Q, T, result) 456 | return result 457 | } 458 | 459 | execExprLiteralRegExp (Q, T) { 460 | this.traceBegin(Q, T) 461 | let result = Q.get("value") 462 | this.traceEnd(Q, T, result) 463 | return result 464 | } 465 | 466 | execExprLiteralNumber (Q, T) { 467 | this.traceBegin(Q, T) 468 | let result = Q.get("value") 469 | this.traceEnd(Q, T, result) 470 | return result 471 | } 472 | 473 | execExprLiteralValue (Q, T) { 474 | this.traceBegin(Q, T) 475 | let result = Q.get("value") 476 | this.traceEnd(Q, T, result) 477 | return result 478 | } 479 | 480 | execExprPath (Q, T) { 481 | this.traceBegin(Q, T) 482 | let result = this.execPath(Q, T) 483 | this.traceEnd(Q, T, result) 484 | return result 485 | } 486 | } 487 | 488 | -------------------------------------------------------------------------------- /src/astq-query-parse.pegjs: -------------------------------------------------------------------------------- 1 | /* 2 | ** ASTq -- Abstract Syntax Tree (AST) Query Engine 3 | ** Copyright (c) 2014-2024 Dr. Ralf S. Engelschall 4 | ** 5 | ** Permission is hereby granted, free of charge, to any person obtaining 6 | ** a copy of this software and associated documentation files (the 7 | ** "Software"), to deal in the Software without restriction, including 8 | ** without limitation the rights to use, copy, modify, merge, publish, 9 | ** distribute, sublicense, and/or sell copies of the Software, and to 10 | ** permit persons to whom the Software is furnished to do so, subject to 11 | ** the following conditions: 12 | ** 13 | ** The above copyright notice and this permission notice shall be included 14 | ** in all copies or substantial portions of the Software. 15 | ** 16 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | { 26 | /* standard PEGUtil integration code */ 27 | var unroll = options.util.makeUnroll(location, options) 28 | var ast = options.util.makeAST (location, options) 29 | } 30 | 31 | /* 32 | ** ==== QUERY ==== 33 | */ 34 | 35 | query 36 | = _ qs:querySet _ eof { 37 | return qs 38 | } 39 | 40 | querySet 41 | = f:queryPath l:(_ "," _ queryPath)* { 42 | return ast("Query").add(unroll(f, l, 3)) 43 | } 44 | 45 | queryPath 46 | = f:queryStep l:(_ queryStepSubsequent)* { 47 | return ast("Path").add(unroll(f, l, 1)) 48 | } 49 | 50 | queryStep 51 | = a:queryAxis? _ m:queryMatch _ r:"!"? _ f:queryFilter? { 52 | return ast("Step").add(a, m, f).set(r !== null ? { isResult: true } : {}) 53 | } 54 | 55 | queryStepSubsequent 56 | = a:queryAxis _ m:queryMatch _ r:"!"? _ f:queryFilter? { 57 | return ast("Step").add(a, m, f).set(r !== null ? { isResult: true } : {}) 58 | } 59 | 60 | queryAxis "axis" 61 | = op:$("//" / "/" / "-//" / "-/" / "+//" / "+/" / "~//" / "~/" / "..//" / "../" / ".//" / "./" / "//") t:queryAxisType? { 62 | return ast("Axis").set({ op: op, type: t !== null ? t : "*" }) 63 | } 64 | 65 | queryAxisType 66 | = ":" id:id { 67 | return id.get("id") 68 | } 69 | / ":" str:string { 70 | return str.get("value") 71 | } 72 | 73 | queryMatch 74 | = id:id { 75 | return ast("Match").merge(id) 76 | } 77 | / str:string { 78 | return ast("Match").set({ id: str.get("value") }) 79 | } 80 | / "*" { 81 | return ast("Match").set({ id: "*" }) 82 | } 83 | 84 | queryFilter 85 | = "[" _ e:expr _ "]" { 86 | return ast("Filter").add(e) 87 | } 88 | 89 | /* 90 | ** ==== EXPRESSION ==== 91 | */ 92 | 93 | expr 94 | = exprConditional 95 | 96 | exprConditional 97 | = e1:exprLogicalOr _ "?:" _ e2:exprConditional { 98 | return ast("ConditionalBinary").add(e1, e2) 99 | } 100 | / e1:exprLogicalOr _ "?" _ e2:exprConditional _ ":" _ e3:exprConditional { 101 | return ast("ConditionalTernary").add(e1, e2, e3) 102 | } 103 | / exprLogicalOr 104 | 105 | exprLogicalOr 106 | = e1:exprLogicalAnd _ op:$("||") _ e2:exprLogicalOr { 107 | return ast("Logical").set({ op: op }).add(e1, e2) 108 | } 109 | / exprLogicalAnd 110 | 111 | exprLogicalAnd 112 | = e1:exprBitwiseOr _ op:$("&&") _ e2:exprLogicalAnd { 113 | return ast("Logical").set({ op: op }).add(e1, e2) 114 | } 115 | / exprBitwiseOr 116 | 117 | exprBitwiseOr 118 | = e1:exprBitwiseXOr _ op:$("|") _ e2:exprBitwiseOr { 119 | return ast("Bitwise").set({ op: op }).add(e1, e2) 120 | } 121 | / exprBitwiseXOr 122 | 123 | exprBitwiseXOr 124 | = e1:exprBitwiseAnd _ op:$("^") _ e2:exprBitwiseXOr { 125 | return ast("Bitwise").set({ op: op }).add(e1, e2) 126 | } 127 | / exprBitwiseAnd 128 | 129 | exprBitwiseAnd 130 | = e1:exprRelational _ op:$("&") _ e2:exprBitwiseAnd { 131 | return ast("Bitwise").set({ op: op }).add(e1, e2) 132 | } 133 | / exprRelational 134 | 135 | exprRelational 136 | = e1:exprBitwiseShift _ op:$("==" / "!=" / "<=" / ">=" / "<" / ">" / "=~" / "!~") _ e2:exprRelational { 137 | return ast("Relational").set({ op: op }).add(e1, e2) 138 | } 139 | / exprBitwiseShift 140 | 141 | exprBitwiseShift 142 | = e1:exprAdditive _ op:$("<<" / ">>") _ e2:exprBitwiseShift { 143 | return ast("Bitwise").set({ op: op }).add(e1, e2) 144 | } 145 | / exprAdditive 146 | 147 | exprAdditive 148 | = e1:exprMultiplicative _ op:$("+" / "-") _ e2:exprAdditive { 149 | return ast("Arithmetical").set({ op: op }).add(e1, e2) 150 | } 151 | / exprMultiplicative 152 | 153 | exprMultiplicative 154 | = e1:exprUnary _ op:$("**" / "*" / "/" / "%") _ e2:exprMultiplicative { 155 | return ast("Arithmetical").set({ op: op }).add(e1, e2) 156 | } 157 | / exprUnary 158 | 159 | exprUnary 160 | = op:$("!" / "~") e:exprOther { 161 | return ast("Unary").set({ op: op }).add(e) 162 | } 163 | / exprOther 164 | 165 | exprOther 166 | = exprFunctionCall 167 | / exprAttribute 168 | / exprParam 169 | / exprLiteral 170 | / exprParenthesis 171 | / exprQuery 172 | 173 | exprFunctionCall 174 | = id:id _ "(" _ p:exprFunctionCallParams? _ ")" { 175 | return ast("FuncCall").merge(id).add(p) 176 | } 177 | 178 | exprFunctionCallParams 179 | = f:expr l:(_ "," _ expr)* { /* RECURSION */ 180 | return unroll(f, l, 3) 181 | } 182 | 183 | exprAttribute "node attribute" 184 | = "@" id:id { 185 | return ast("Attribute").merge(id) 186 | } 187 | / "@" str:string { 188 | return ast("Attribute").set({ id: str.get("value") }) 189 | } 190 | 191 | exprParam "query parameter reference" 192 | = "{" _ name:id _ "}" { 193 | return ast("Param").merge(name) 194 | } 195 | 196 | exprLiteral 197 | = string 198 | / regexp 199 | / number 200 | / value 201 | 202 | exprParenthesis 203 | = "(" _ e:expr _ ")" { /* RECURSION */ 204 | return e 205 | } 206 | 207 | exprQuery 208 | = queryPath /* RECURSION */ 209 | 210 | /* 211 | ** ==== LITERALS ==== 212 | */ 213 | 214 | id "identifier" 215 | = id:$(!value [a-zA-Z_][a-zA-Z0-9_-]*) { 216 | return ast("Identifier").set({ id: id }) 217 | } 218 | 219 | string "quoted string literal" 220 | = "\"" s:((stringEscapedCharDQ / [^"])*) "\"" { 221 | return ast("LiteralString").set({ value: s.join("") }) 222 | } 223 | / "'" s:((stringEscapedCharSQ / [^'])*) "'" { 224 | return ast("LiteralString").set({ value: s.join("") }) 225 | } 226 | 227 | stringEscapedCharDQ "escaped double-quoted-string character" 228 | = "\\\\" { return "\\" } 229 | / "\\\"" { return "\"" } 230 | / "\\b" { return "\b" } 231 | / "\\v" { return "\x0B" } 232 | / "\\f" { return "\f" } 233 | / "\\t" { return "\t" } 234 | / "\\r" { return "\r" } 235 | / "\\n" { return "\n" } 236 | / "\\e" { return "\x1B" } 237 | / "\\x" n:$([0-9a-fA-F][0-9a-fA-F]) { 238 | return String.fromCharCode(parseInt(n, 16)) 239 | } 240 | / "\\u" n:$([0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]) { 241 | return String.fromCharCode(parseInt(n, 16)) 242 | } 243 | 244 | stringEscapedCharSQ "escaped single-quoted-string character" 245 | = "\\'" { return "'" } 246 | 247 | regexp "regular expression literal" 248 | = "`" re:$(("\\`" / [^`])*) "`" { 249 | var v 250 | try { v = new RegExp(re.replace(/\\`/g, "`")) } 251 | catch (e) { error(e.message) } 252 | return ast("LiteralRegExp").set({ value: v }) 253 | } 254 | 255 | number "numeric literal" 256 | = s:$([+-]?) "0b" n:$([01]+) { 257 | return ast("LiteralNumber").set({ value: parseInt(s + n, 2) }) 258 | } 259 | / s:$([+-]?) "0o" n:$([0-7]+) { 260 | return ast("LiteralNumber").set({ value: parseInt(s + n, 8) }) 261 | } 262 | / s:$([+-]?) "0x" n:$([0-9a-fA-F]+) { 263 | return ast("LiteralNumber").set({ value: parseInt(s + n, 16) }) 264 | } 265 | / n:$([+-]? [0-9]* "." [0-9]+ ([eE] [+-]? [0-9]+)?) { 266 | return ast("LiteralNumber").set({ value: parseFloat(n) }) 267 | } 268 | / n:$([+-]? [0-9]+) { 269 | return ast("LiteralNumber").set({ value: parseInt(n, 10) }) 270 | } 271 | 272 | value "global value" 273 | = "true" { return ast("LiteralValue").set({ value: true }) } 274 | / "false" { return ast("LiteralValue").set({ value: false }) } 275 | / "null" { return ast("LiteralValue").set({ value: null }) } 276 | / "NaN" { return ast("LiteralValue").set({ value: NaN }) } 277 | / "undefined" { return ast("LiteralValue").set({ value: undefined }) } 278 | 279 | /* 280 | ** ==== GLUE ==== 281 | */ 282 | 283 | _ "optional blank" 284 | = (co / ws)* 285 | 286 | co "multi-line comment" 287 | = "/*" (!"*/" .)* "*/" 288 | 289 | ws "any whitespaces" 290 | = [ \t\r\n]+ 291 | 292 | eof "end of file" 293 | = !. 294 | 295 | -------------------------------------------------------------------------------- /src/astq-query-trace.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** ASTq -- Abstract Syntax Tree (AST) Query Engine 3 | ** Copyright (c) 2014-2024 Dr. Ralf S. Engelschall 4 | ** 5 | ** Permission is hereby granted, free of charge, to any person obtaining 6 | ** a copy of this software and associated documentation files (the 7 | ** "Software"), to deal in the Software without restriction, including 8 | ** without limitation the rights to use, copy, modify, merge, publish, 9 | ** distribute, sublicense, and/or sell copies of the Software, and to 10 | ** permit persons to whom the Software is furnished to do so, subject to 11 | ** the following conditions: 12 | ** 13 | ** The above copyright notice and this permission notice shall be included 14 | ** in all copies or substantial portions of the Software. 15 | ** 16 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | /* eslint no-console: 0 */ 26 | 27 | import util from "./astq-util.js" 28 | 29 | export default class ASTQQueryTrace { 30 | /* determine output prefix based on tree depth */ 31 | prefixOf (Q, T) { 32 | let depth = 0 33 | let node = Q 34 | while ((node = node.parent()) !== null) 35 | depth++ 36 | let prefix1 = util.pad("", 4 * depth) 37 | depth = 0 38 | node = T 39 | while ((node = this.adapter.getParentNode(node, "*")) !== null) 40 | depth++ 41 | let prefix2 = util.pad("", 4 * depth) 42 | return { prefix1, prefix2 } 43 | } 44 | 45 | /* begin tracing step */ 46 | traceBegin (Q, T) { 47 | if (!this.trace) 48 | return 49 | let { prefix1, prefix2 } = this.prefixOf(Q, T) 50 | console.log("ASTQ: execute: | " + 51 | util.pad(prefix1 + Q.type() + " (", -60) + " | " + 52 | prefix2 + this.adapter.getNodeType(T)) 53 | } 54 | 55 | /* end tracing step */ 56 | traceEnd (Q, T, val) { 57 | if (!this.trace) 58 | return 59 | let { prefix1, prefix2 } = this.prefixOf(Q, T) 60 | let result 61 | if (val === undefined) 62 | result = "undefined" 63 | else if (typeof val === "object" && val instanceof Array) { 64 | result = "[" 65 | val.forEach((node) => { 66 | result += "node(" + this.adapter.getNodeType(node) + ")," 67 | }) 68 | result = result.replace(/,$/, "") + "]" 69 | } 70 | else 71 | result = typeof val + "(" + val + ")" 72 | if (result.length > 60) 73 | result = result.substr(0, 60) + "..." 74 | console.log("ASTQ: execute: | " + 75 | util.pad(prefix1 + "): " + result, -60) + " | " + 76 | prefix2 + this.adapter.getNodeType(T)) 77 | } 78 | } 79 | 80 | -------------------------------------------------------------------------------- /src/astq-query.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** ASTq -- Abstract Syntax Tree (AST) Query Engine 3 | ** Copyright (c) 2014-2024 Dr. Ralf S. Engelschall 4 | ** 5 | ** Permission is hereby granted, free of charge, to any person obtaining 6 | ** a copy of this software and associated documentation files (the 7 | ** "Software"), to deal in the Software without restriction, including 8 | ** without limitation the rights to use, copy, modify, merge, publish, 9 | ** distribute, sublicense, and/or sell copies of the Software, and to 10 | ** permit persons to whom the Software is furnished to do so, subject to 11 | ** the following conditions: 12 | ** 13 | ** The above copyright notice and this permission notice shall be included 14 | ** in all copies or substantial portions of the Software. 15 | ** 16 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | /* eslint no-console: off */ 26 | 27 | /* load external dependencies */ 28 | import ASTY from "asty" 29 | import PEGUtil from "pegjs-util" 30 | 31 | /* get query executor */ 32 | import ASTQQueryExec from "./astq-query-exec.js" 33 | 34 | /* get query parser (by loading and on-the-fly compiling PEG.js grammar) */ 35 | const PEG = require("pegjs-otf") 36 | const ASTQQueryParse = PEG.generateFromFile( 37 | /* eslint n/no-path-concat: off */ 38 | __dirname + "/astq-query-parse.pegjs", 39 | { optimize: "speed", cache: true } 40 | ) 41 | 42 | export default class ASTQQuery { 43 | /* create a new instance of the query instance */ 44 | constructor (selector) { 45 | this.asty = new ASTY() 46 | this.ast = null 47 | if (selector) 48 | this.compile(selector) 49 | } 50 | 51 | /* compile query selector into AST */ 52 | compile (selector, trace) { 53 | if (trace) 54 | console.log("ASTQ: compile: +---------------------------------------" + 55 | "----------------------------------------------------------------\n" + 56 | "ASTQ: compile: | " + selector) 57 | let result = PEGUtil.parse(ASTQQueryParse, selector, { 58 | startRule: "query", 59 | makeAST: (line, column, offset, args) => { 60 | return this.asty.create.apply(this.asty, args).pos(line, column, offset) 61 | } 62 | }) 63 | if (result.error !== null) 64 | throw new Error("ASTQ: compile: query parsing failed:\n" + 65 | PEGUtil.errorMessage(result.error, true).replace(/^/mg, "ERROR: ")) 66 | this.ast = result.ast 67 | if (trace) 68 | console.log("ASTQ: compile: +---------------------------------------" + 69 | "----------------------------------------------------------------\n" + 70 | this.dump().replace(/\n$/, "").replace(/^/mg, "ASTQ: compile: | ")) 71 | return this 72 | } 73 | 74 | /* dump the query AST */ 75 | dump () { 76 | return this.ast.dump() 77 | } 78 | 79 | /* execute the query AST onto node */ 80 | execute (node, adapter, params, funcs, trace) { 81 | if (trace) 82 | console.log("ASTQ: execute: +---------------------------------------" + 83 | "----------------------------------------------------------------") 84 | let qe = new ASTQQueryExec(adapter, params, funcs, trace) 85 | return qe.execQuery(this.ast, node) 86 | } 87 | } 88 | 89 | -------------------------------------------------------------------------------- /src/astq-util.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** ASTq -- Abstract Syntax Tree (AST) Query Engine 3 | ** Copyright (c) 2014-2024 Dr. Ralf S. Engelschall 4 | ** 5 | ** Permission is hereby granted, free of charge, to any person obtaining 6 | ** a copy of this software and associated documentation files (the 7 | ** "Software"), to deal in the Software without restriction, including 8 | ** without limitation the rights to use, copy, modify, merge, publish, 9 | ** distribute, sublicense, and/or sell copies of the Software, and to 10 | ** permit persons to whom the Software is furnished to do so, subject to 11 | ** the following conditions: 12 | ** 13 | ** The above copyright notice and this permission notice shall be included 14 | ** in all copies or substantial portions of the Software. 15 | ** 16 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | export default class ASTQUtil { 26 | /* pad a string with spaces to the left/right */ 27 | static pad (str, num) { 28 | let n = num < 0 ? -num : num 29 | if (str.length > n) 30 | str = str.substr(0, n) 31 | else { 32 | let pad = Array((n + 1) - str.length).join(" ") 33 | str = num < 0 ? (str + pad) : (pad + str) 34 | } 35 | return str 36 | } 37 | 38 | /* check whether value is "true" (or can be considered to be true) */ 39 | static truthy (value) { 40 | let result 41 | switch (typeof value) { 42 | case "boolean": 43 | result = value 44 | break 45 | case "number": 46 | result = (value !== 0 && !isNaN(value)) 47 | break 48 | case "string": 49 | result = (value !== "") 50 | break 51 | case "object": 52 | result = false 53 | if (value !== null) { 54 | result = true 55 | if (value instanceof Array) 56 | result = value.length > 0 57 | } 58 | break 59 | default: 60 | result = false 61 | } 62 | return result 63 | } 64 | 65 | /* coerce value to particular type */ 66 | static coerce (value, type) { 67 | /* eslint valid-typeof: off */ 68 | if (typeof value !== type) { 69 | try { 70 | switch (type) { 71 | case "boolean": 72 | if (typeof value === "object" && value instanceof Array) 73 | value = value.length !== 0 74 | else if (typeof value !== "boolean") 75 | value = Boolean(value) 76 | break 77 | case "number": 78 | if (typeof value === "object" && value instanceof Array) 79 | value = value.length 80 | else if (typeof value !== "number") 81 | value = Number(value) 82 | break 83 | case "string": 84 | if (typeof value !== "string") 85 | value = String(value) 86 | break 87 | case "regexp": 88 | if (!(typeof value === "object" && value instanceof RegExp)) 89 | value = new RegExp(value) 90 | break 91 | } 92 | } 93 | catch (e) { 94 | throw new Error("cannot coerce value into type " + type) 95 | } 96 | } 97 | return value 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/astq-version.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** ASTq -- Abstract Syntax Tree (AST) Query Engine 3 | ** Copyright (c) 2014-2024 Dr. Ralf S. Engelschall 4 | ** 5 | ** Permission is hereby granted, free of charge, to any person obtaining 6 | ** a copy of this software and associated documentation files (the 7 | ** "Software"), to deal in the Software without restriction, including 8 | ** without limitation the rights to use, copy, modify, merge, publish, 9 | ** distribute, sublicense, and/or sell copies of the Software, and to 10 | ** permit persons to whom the Software is furnished to do so, subject to 11 | ** the following conditions: 12 | ** 13 | ** The above copyright notice and this permission notice shall be included 14 | ** in all copies or substantial portions of the Software. 15 | ** 16 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | /* global $major: false */ 26 | /* global $minor: false */ 27 | /* global $micro: false */ 28 | /* global $date: false */ 29 | 30 | let version = { 31 | major: $major, 32 | minor: $minor, 33 | micro: $micro, 34 | date: $date 35 | } 36 | 37 | export default version 38 | 39 | -------------------------------------------------------------------------------- /src/astq.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** ASTq -- Abstract Syntax Tree (AST) Query Engine 3 | ** Copyright (c) 2014-2024 Dr. Ralf S. Engelschall 4 | ** 5 | ** Permission is hereby granted, free of charge, to any person obtaining 6 | ** a copy of this software and associated documentation files (the 7 | ** "Software"), to deal in the Software without restriction, including 8 | ** without limitation the rights to use, copy, modify, merge, publish, 9 | ** distribute, sublicense, and/or sell copies of the Software, and to 10 | ** permit persons to whom the Software is furnished to do so, subject to 11 | ** the following conditions: 12 | ** 13 | ** The above copyright notice and this permission notice shall be included 14 | ** in all copies or substantial portions of the Software. 15 | ** 16 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | /* load external depdendencies */ 26 | import CacheLRU from "cache-lru" 27 | 28 | /* load internal dependencies */ 29 | import ASTQAdapter from "./astq-adapter.js" 30 | import ASTQAdapterXMLDOM from "./astq-adapter-xmldom.js" 31 | import ASTQAdapterPARSE5 from "./astq-adapter-parse5.js" 32 | import ASTQAdapterMOZAST from "./astq-adapter-mozast.js" 33 | import ASTQAdapterGRAPHQL from "./astq-adapter-graphql.js" 34 | import ASTQAdapterJSON from "./astq-adapter-json.js" 35 | import ASTQAdapterCHEERIO from "./astq-adapter-cheerio.js" 36 | import ASTQAdapterUNIST from "./astq-adapter-unist.js" 37 | import ASTQAdapterASTY from "./astq-adapter-asty.js" 38 | import ASTQFuncs from "./astq-funcs.js" 39 | import ASTQFuncsSTD from "./astq-funcs-std.js" 40 | import ASTQQuery from "./astq-query.js" 41 | import ASTQVersion from "./astq-version.js" 42 | 43 | /* define the API class */ 44 | class ASTQ { 45 | /* create a new ASTq instance */ 46 | constructor () { 47 | /* create adapter registry and pre-register standard adapters */ 48 | this._adapter = new ASTQAdapter() 49 | .register(ASTQAdapterXMLDOM, false) 50 | .register(ASTQAdapterPARSE5, false) 51 | .register(ASTQAdapterMOZAST, false) 52 | .register(ASTQAdapterGRAPHQL, false) 53 | .register(ASTQAdapterJSON, false) 54 | .register(ASTQAdapterCHEERIO, false) 55 | .register(ASTQAdapterUNIST, false) 56 | .register(ASTQAdapterASTY, false) 57 | 58 | /* create function registry and pre-register standard functions */ 59 | this._funcs = new ASTQFuncs() 60 | for (let name in ASTQFuncsSTD) 61 | this.func(name, ASTQFuncsSTD[name]) 62 | 63 | /* create LRU cache */ 64 | this._cache = new CacheLRU() 65 | } 66 | 67 | /* return the version information */ 68 | version () { 69 | return ASTQVersion 70 | } 71 | 72 | /* switch to a single custom adapter */ 73 | adapter (adapter, force = false) { 74 | if (arguments.length < 1 || arguments.length > 2) 75 | throw new Error("ASTQ#adapter: invalid number of arguments") 76 | this._adapter.unregister() 77 | if (!(typeof adapter === "object" && adapter instanceof Array)) 78 | adapter = [ adapter ] 79 | if (adapter.length > 1 && force) 80 | throw new Error("ASTQ#adapter: you can force just a single adapter to not taste the AST node") 81 | adapter.forEach((adapter) => { 82 | if (typeof adapter === "string") { 83 | if (adapter === "mozast") adapter = ASTQAdapterMOZAST 84 | else if (adapter === "graphql") adapter = ASTQAdapterGRAPHQL 85 | else if (adapter === "xmldom") adapter = ASTQAdapterXMLDOM 86 | else if (adapter === "parse5") adapter = ASTQAdapterPARSE5 87 | else if (adapter === "json") adapter = ASTQAdapterJSON 88 | else if (adapter === "cheerio") adapter = ASTQAdapterCHEERIO 89 | else if (adapter === "unist") adapter = ASTQAdapterUNIST 90 | else if (adapter === "asty") adapter = ASTQAdapterASTY 91 | else 92 | throw new Error("ASTQ#adapter: unknown built-in adapter") 93 | } 94 | this._adapter.register(adapter, force) 95 | }) 96 | return this 97 | } 98 | 99 | /* register an additional function */ 100 | func (name, func) { 101 | if (arguments.length !== 2) 102 | throw new Error("ASTQ#func: invalid number of arguments") 103 | this._funcs.register(name, func) 104 | return this 105 | } 106 | 107 | /* configure the LRU cache limit */ 108 | cache (entries) { 109 | if (arguments.length !== 1) 110 | throw new Error("ASTQ#cache: invalid number of arguments") 111 | this._cache.limit(entries) 112 | return this 113 | } 114 | 115 | /* individual step 1: compile selector DSL into a query AST */ 116 | compile (selector, trace) { 117 | if (arguments.length < 1) 118 | throw new Error("ASTQ#compile: too less arguments") 119 | if (arguments.length > 2) 120 | throw new Error("ASTQ#compile: too many arguments") 121 | if (trace === undefined) 122 | trace = false 123 | let query = this._cache.get(selector) 124 | if (query === undefined) { 125 | query = new ASTQQuery() 126 | query.compile(selector, trace) 127 | this._cache.set(selector, query) 128 | } 129 | return query 130 | } 131 | 132 | /* individual step 2: execute query AST onto node */ 133 | execute (node, query, params, trace) { 134 | if (arguments.length < 2) 135 | throw new Error("ASTQ#execute: too less arguments") 136 | if (arguments.length > 4) 137 | throw new Error("ASTQ#execute: too many arguments") 138 | if (params === undefined) 139 | params = {} 140 | if (trace === undefined) 141 | trace = false 142 | let adapter = this._adapter.select(node) 143 | if (adapter === undefined) 144 | throw new Error("ASTQ#execute: no suitable adapter found for node") 145 | return query.execute(node, adapter, params, this._funcs, trace) 146 | } 147 | 148 | /* all-in-one step: execute selector DSL onto node */ 149 | query (node, selector, params, trace) { 150 | if (arguments.length < 2) 151 | throw new Error("ASTQ#query: too less arguments") 152 | if (arguments.length > 4) 153 | throw new Error("ASTQ#query: too many arguments") 154 | if (params === undefined) 155 | params = {} 156 | if (trace === undefined) 157 | trace = false 158 | return this.execute(node, this.compile(selector, trace), params, trace) 159 | } 160 | } 161 | 162 | /* export the traditional way for interoperability reasons 163 | (as Babel would export an object with a 'default' field) */ 164 | module.exports = ASTQ 165 | 166 | -------------------------------------------------------------------------------- /tst/astq.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** ASTq -- Abstract Syntax Tree (AST) Query Engine 3 | ** Copyright (c) 2014-2024 Dr. Ralf S. Engelschall 4 | ** 5 | ** Permission is hereby granted, free of charge, to any person obtaining 6 | ** a copy of this software and associated documentation files (the 7 | ** "Software"), to deal in the Software without restriction, including 8 | ** without limitation the rights to use, copy, modify, merge, publish, 9 | ** distribute, sublicense, and/or sell copies of the Software, and to 10 | ** permit persons to whom the Software is furnished to do so, subject to 11 | ** the following conditions: 12 | ** 13 | ** The above copyright notice and this permission notice shall be included 14 | ** in all copies or substantial portions of the Software. 15 | ** 16 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | /* global describe: false */ 26 | /* global it: false */ 27 | /* jshint -W030 */ 28 | /* eslint no-unused-expressions: 0 */ 29 | 30 | const chai = require("chai") 31 | const expect = chai.expect 32 | chai.config.includeStack = true 33 | 34 | const ASTQ = require("../lib/astq.node.js") 35 | 36 | describe("ASTq Library", function () { 37 | const astq = new ASTQ() 38 | 39 | it("API availability", function () { 40 | expect(astq).to.respondTo("version") 41 | expect(astq).to.respondTo("adapter") 42 | expect(astq).to.respondTo("func") 43 | expect(astq).to.respondTo("cache") 44 | expect(astq).to.respondTo("compile") 45 | expect(astq).to.respondTo("execute") 46 | expect(astq).to.respondTo("query") 47 | expect(astq.version()).to.have.property("major") 48 | expect(astq.version()).to.have.property("minor") 49 | expect(astq.version()).to.have.property("micro") 50 | expect(astq.version()).to.have.property("date") 51 | }) 52 | 53 | astq.func("add", function (a, b) { return a + b }) 54 | 55 | /* 56 | * node1 57 | * node2 58 | * node3 59 | * node5 60 | * node6 61 | * node8 62 | * node9 63 | * node7 64 | * node4 65 | */ 66 | const ASTY = require("asty") 67 | const asty = new ASTY() 68 | const node1 = asty.create("node1") 69 | const node2 = asty.create("node2") 70 | const node3 = asty.create("node3") 71 | const node4 = asty.create("node4") 72 | const node5 = asty.create("node5") 73 | const node6 = asty.create("node6").set("foo", "bar") 74 | const node7 = asty.create("node7").set("quux", "baz") 75 | const node8 = asty.create("node8") 76 | const node9 = asty.create("node9") 77 | node1.add(node2) 78 | node1.add(node3) 79 | node1.add(node4) 80 | node3.add(node5) 81 | node3.add(node6) 82 | node3.add(node7) 83 | node6.add(node8) 84 | node6.add(node9) 85 | 86 | it("simple queries", function () { 87 | expect(astq.query(node1, "node1")).to.have.members([ node1 ]) 88 | expect(astq.query(node1, "badNodeName")).to.be.empty 89 | expect(astq.query(node1, "*")).to.have.members([ node1 ]) 90 | }) 91 | 92 | it("axis queries", function () { 93 | expect(astq.query(node1, "/ *")) 94 | .to.be.deep.equal([ node2, node3, node4 ]) 95 | expect(astq.query(node1, "// *")) 96 | .to.be.deep.equal([ node2, node3, node5, node6, node8, node9, node7, node4 ]) 97 | expect(astq.query(node1, "./ *")) 98 | .to.be.deep.equal([ node1, node2, node3, node4 ]) 99 | expect(astq.query(node1, ".// *")) 100 | .to.be.deep.equal([ node1, node2, node3, node5, node6, node8, node9, node7, node4 ]) 101 | expect(astq.query(node3, "-/ *")) 102 | .to.be.deep.equal([ node2 ]) 103 | expect(astq.query(node4, "-// *")) 104 | .to.be.deep.equal([ node3, node2 ]) 105 | expect(astq.query(node3, "+/ *")) 106 | .to.be.deep.equal([ node4 ]) 107 | expect(astq.query(node2, "+// *")) 108 | .to.be.deep.equal([ node3, node4 ]) 109 | expect(astq.query(node3, "~/ *")) 110 | .to.be.deep.equal([ node2, node4 ]) 111 | expect(astq.query(node3, "~// *")) 112 | .to.be.deep.equal([ node2, node4 ]) 113 | expect(astq.query(node6, "../ *")) 114 | .to.be.deep.equal([ node3 ]) 115 | expect(astq.query(node6, "..// *")) 116 | .to.be.deep.equal([ node3, node1 ]) 117 | expect(astq.query(node6, "// *")) 120 | .to.be.deep.equal([ node8, node9, node7, node4 ]) 121 | }) 122 | 123 | it("filter functions", function () { 124 | expect(astq.query(node1, "// * [ type() == 'node3' ]")) 125 | .to.have.members([ node3 ]) 126 | expect(astq.query(node1, "// * [ depth() == 3 ]")) 127 | .to.have.members([ node5, node6, node7 ]) 128 | expect(astq.query(node1, "// * [ pos() <= 1 ]")) 129 | .to.have.members([ node2, node5, node8 ]) 130 | expect(astq.query(node1, "// * [ nth(2) ]")) 131 | .to.have.members([ node3, node6, node9 ]) 132 | expect(astq.query(node1, "// * [ first() ]")) 133 | .to.have.members([ node2, node5, node8 ]) 134 | expect(astq.query(node1, "// * [ last() ]")) 135 | .to.have.members([ node4, node7, node9 ]) 136 | expect(astq.query(node1, "// * [ count(/*) == 3 ]")) 137 | .to.have.members([ node3 ]) 138 | expect(astq.query(node1, "// * [ below({node}) ]", { node: node3 })) 139 | .to.have.members([ node5, node6, node7, node8, node9 ]) 140 | expect(astq.query(node1, "// * [ follows({node}) ]", { node: node6 })) 141 | .to.be.deep.equal([ node8, node9, node7, node4 ]) 142 | expect(astq.query(node1, "// * [ in({nodes}) ]", { nodes: [ node3, node6 ] })) 143 | .to.have.members([ node3, node6 ]) 144 | expect(astq.query(node1, "// * [ in( * [ @foo == 'bar' || @quux == 'baz' ]) ]")) 145 | .to.have.members([ node6, node7 ]) 146 | }) 147 | 148 | it("complex queries", function () { 149 | expect(astq.query(node1, "* [ * [ * [ node1 ]]]")) 150 | .to.have.members([ node1 ]) 151 | expect(astq.query(node1, "// * [ @foo == 'bar' ]")) 152 | .to.have.members([ node6 ]) 153 | expect(astq.query(node1, "// * [ @foo == 'bar' ] +// * [ @quux == 'baz' ]")) 154 | .to.have.members([ node7 ]) 155 | expect(astq.query(node1, "// * [ @foo == 'bar' && +// * [ @quux == 'baz' ] ]")) 156 | .to.have.members([ node6 ]) 157 | expect(astq.query(node1, "/ node2 ../ node1 / node2")) 158 | .to.have.members([ node2 ]) 159 | }) 160 | 161 | it("subset queries", function () { 162 | expect(astq.query(node1, "*, // *")) 163 | .to.have.members([ node1, node2, node3, node5, node6, node8, node9, node7, node4 ]) 164 | expect(astq.query(node6, "// *")).to.be.deep.equal([ node8, node9, node7, node4 ]) 166 | }) 167 | 168 | it("quoted identifiers", function () { 169 | expect(astq.query(node1, "// \"node3\"")).to.be.deep.equal([ node3 ]) 170 | expect(astq.query(node1, "// * [ @\"foo\" ]")).to.be.deep.equal([ node6 ]) 171 | }) 172 | 173 | it("result marking", function () { 174 | expect(astq.query(node1, "* ! / * / * /*")).to.be.deep.equal([ node1 ]) 175 | expect(astq.query(node1, "* / * ! / * /*")).to.be.deep.equal([ node2, node3, node4 ]) 176 | expect(astq.query(node1, "* / * / * ! /*")).to.be.deep.equal([ node5, node6, node7 ]) 177 | expect(astq.query(node1, "* / * / * / * !")).to.be.deep.equal([ node8, node9 ]) 178 | expect(astq.query(node1, "* / * ! [ count(/ * ! / *) == 3 ]")).to.be.deep.equal([ node3 ]) 179 | }) 180 | }) 181 | 182 | --------------------------------------------------------------------------------