├── .github └── workflows │ └── main.yml ├── .gitignore ├── .rspec ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── Guardfile ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── lib ├── node_query.rb ├── node_query │ ├── adapter.rb │ ├── adapter │ │ ├── parser.rb │ │ ├── prism.rb │ │ └── syntax_tree.rb │ ├── compiler.rb │ ├── compiler │ │ ├── array_value.rb │ │ ├── attribute.rb │ │ ├── attribute_list.rb │ │ ├── basic_selector.rb │ │ ├── boolean.rb │ │ ├── comparable.rb │ │ ├── expression.rb │ │ ├── expression_list.rb │ │ ├── float.rb │ │ ├── identifier.rb │ │ ├── integer.rb │ │ ├── invalid_operator_error.rb │ │ ├── nil.rb │ │ ├── parse_error.rb │ │ ├── regexp.rb │ │ ├── selector.rb │ │ ├── string.rb │ │ └── symbol.rb │ ├── helper.rb │ ├── node_rules.rb │ └── version.rb ├── node_query_lexer.rex └── node_query_parser.y ├── node_query.gemspec ├── sig ├── node_query.rbs └── node_query │ └── adapter.rbs └── spec ├── node_query ├── adapter │ ├── parser_spec.rb │ ├── prism_spec.rb │ └── syntax_tree_spec.rb ├── helper_spec.rb └── node_rules │ ├── parser_spec.rb │ ├── prism_spec.rb │ └── syntax_tree_spec.rb ├── node_query_lexer_spec.rb ├── node_query_parser ├── parser_spec.rb ├── prism_spec.rb └── syntax_tree_spec.rb ├── node_query_parser_spec.rb ├── node_query_spec.rb ├── spec_helper.rb └── support └── parser_helper.rb /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Test & deploy documentation 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | ruby-version: ['2.7', '3.0', '3.1', '3.2', '3.3', '3.4'] 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Set up Ruby 19 | uses: ruby/setup-ruby@v1 20 | with: 21 | ruby-version: ${{ matrix.ruby-version }} 22 | bundler-cache: true 23 | - name: Run tests 24 | run: bundle exec rake 25 | 26 | deploy: 27 | runs-on: ubuntu-latest 28 | needs: test 29 | name: Update gh-pages to docs 30 | steps: 31 | - uses: actions/checkout@v3 32 | - uses: ruby/setup-ruby@v1 33 | with: 34 | ruby-version: '3.4' 35 | bundler-cache: true 36 | - name: Install required gem dependencies 37 | run: gem install yard --no-document 38 | - name: Build YARD Ruby Documentation 39 | run: yardoc --output-dir docs 40 | - name: Deploy 41 | uses: peaceiris/actions-gh-pages@v3 42 | with: 43 | deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} 44 | publish_dir: ./docs 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | # rspec failure tracking 11 | .rspec_status 12 | 13 | lib/node_query_lexer.rex.rb 14 | lib/node_query_parser.racc.rb 15 | lib/node_query_parser.output 16 | 17 | .vscode/ 18 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 1.16.0 (2024-12-27) 4 | 5 | * Support `prism` 1.3.0 6 | 7 | ## 1.15.4 (2024-07-02) 8 | 9 | * Drop support ruby 2.6 10 | * Update `prism_ext` to 0.3.3 11 | * Use `Prism::Node#to_source` 12 | 13 | ## 1.15.3 (2024-05-30) 14 | 15 | * Fix actual value is nil 16 | 17 | ## 1.15.2 (2024-04-07) 18 | 19 | * Update `prism_ext` to 0.3.0 20 | * Update `syntax_tree_ext` to 0.8.1 21 | * Update `parser_node_ext` to 1.3.0 22 | * Require `parent_node_ext` only when necessary 23 | 24 | ## 1.15.1 (2024-02-16) 25 | 26 | * Use `deconstruct_keys` to get prism children 27 | * Get `PrismAdapter` 28 | * Read prism node type 29 | 30 | ## 1.15.0 (2024-02-10) 31 | 32 | * Support `prism` 33 | 34 | ## 1.14.1 (2023-11-27) 35 | 36 | * Fix `ArrayValue` with `adapter` parameter 37 | 38 | ## 1.14.0 (2023-11-27) 39 | 40 | * Add `adapter` parameter to `NodeQuery` 41 | * Do not allow to configure an `adapter` globally 42 | 43 | ## 1.13.12 (2023-09-29) 44 | 45 | * `NODE_TYPE` can contain `_` 46 | * Update `syntax_tree_ext` to 0.6.4 47 | 48 | ## 1.13.11 (2023-08-17) 49 | 50 | * Do not handle if `child_node` is nil 51 | 52 | ## 1.13.10 (2023-08-12) 53 | 54 | * Update `node_query_parser.y` 55 | 56 | ## 1.13.9 (2023-08-02) 57 | 58 | * Add `OPERATOR` macro 59 | * Use operator `=` instead of `==` 60 | 61 | ## 1.13.8 (2023-06-28) 62 | 63 | * Check `.to_value` instead of `.type` 64 | 65 | ## 1.13.7 (2023-06-26) 66 | 67 | * Revert "Flatten syntax_tree children" 68 | 69 | ## 1.13.6 (2023-06-26) 70 | 71 | * Flatten syntax_tree children 72 | 73 | ## 1.13.5 (2023-06-17) 74 | 75 | * Separate syntax_tree tests from parser tests 76 | * Flatten child iteration for syntax_tree 77 | 78 | ## 1.13.4 (2023-06-15) 79 | 80 | * Support `Hash#except` for ruby 2 81 | 82 | ## 1.13.3 (2023-06-14) 83 | 84 | * Use `deconstruct_key` to get syntax_tree node children 85 | * `handle_recursive_child` handles Array child node 86 | 87 | ## 1.13.2 (2023-05-18) 88 | 89 | * Replace `Parser` specific code 90 | 91 | ## 1.13.1 (2023-05-16) 92 | 93 | * Require `parser` and `syntax_tree` in adapter 94 | * `SyntaxTreeParser#get_node_type` returns a symbol 95 | * Node type can be upcase 96 | 97 | ## 1.13.0 (2023-05-15) 98 | 99 | * Add `SyntaxTreeParser` 100 | 101 | ## 1.12.1 (2023-04-06) 102 | 103 | * Fix when `actual` is nil 104 | 105 | ## 1.12.0 (2023-01-16) 106 | 107 | * Drop `activesupport` 108 | * Remove `NodeQuery::AnyValue` 109 | 110 | ## 1.11.0 (2022-12-09) 111 | 112 | * Support negative index to fetch array element 113 | * Parse negative integer and float 114 | 115 | ## 1.10.0 (2022-10-26) 116 | 117 | * Add `NodeQuery::MethodNotSupported` error 118 | * Add `NodeQuery::AnyValue` to match any value in node rules 119 | 120 | ## 1.9.0 (2022-10-23) 121 | 122 | * Support `NOT INCLUDES` operator 123 | * `includes` / `not_includes` a selector 124 | 125 | ## 1.8.1 (2022-10-15) 126 | 127 | * Fix `filter_by_position` with empty nodes 128 | 129 | ## 1.8.0 (2022-10-14) 130 | 131 | * Support `:first-child` and `:last-child` 132 | 133 | ## 1.7.0 (2022-10-01) 134 | 135 | * Better regexp to match evaluated value 136 | * Make `base_node` as the root matching node 137 | 138 | ## 1.6.1 (2022-09-28) 139 | 140 | * Do not handle `erange` and `irange` in `actual_value` 141 | 142 | ## 1.6.0 (2022-09-16) 143 | 144 | * Rename `nodeType` to `node_type` 145 | 146 | ## 1.5.0 (2022-09-15) 147 | 148 | * Add `Helper.to_string` 149 | * Only check the current node if `including_self` is true and `recursive` is false 150 | * Fix `Regexp#match?` and `String#match?` 151 | * Rename `stop_on_match` to `stop_at_first_match` 152 | 153 | ## 1.4.0 (2022-09-14) 154 | 155 | * Add options `including_self`, `stop_on_match` and `recursive` 156 | * Fix regex to match evaluated value 157 | 158 | ## 1.3.0 (2022-09-13) 159 | 160 | * Rename `NodeQuery#parse` to `NodeQuery#query_nodes` 161 | * `NodeQuery#query_ndoes` accepts `including_self` argument 162 | * `NodeQuery#query_ndoes` supports both nql and rules 163 | * Add `NodeQuery#match_node?` 164 | * Add `NdoeRules` 165 | * Drop `EvaluatedValue`, use `String` instead 166 | * Write better test cases 167 | 168 | ## 1.2.0 (2022-07-01) 169 | 170 | * Rename `NodeQuery.get_adapter` to `NodeQuery.adapter` 171 | * Use generic type in rbs 172 | * Fix `Compiler::Array` to `Compiler::ArrayValue` 173 | 174 | ## 1.1.0 (2022-06-27) 175 | 176 | * Support `*` in attribute key 177 | * Add new Adapter method `is_node?` 178 | 179 | ## 1.0.0 (2022-06-26) 180 | 181 | * Abstract from synvert-core 182 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in node_query.gemspec 6 | gemspec 7 | 8 | gem "rake", "~> 13.0" 9 | 10 | gem "rspec", "~> 3.0" 11 | 12 | gem "guard" 13 | gem "guard-rspec" 14 | gem "guard-rake" 15 | gem "oedipus_lex" 16 | gem "racc" 17 | gem "parser_node_ext" 18 | gem "syntax_tree_ext" 19 | gem "prism_ext" 20 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | node_query (1.16.0) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | ast (2.4.2) 10 | coderay (1.1.3) 11 | diff-lcs (1.5.0) 12 | ffi (1.15.5) 13 | formatador (1.1.0) 14 | guard (2.18.0) 15 | formatador (>= 0.2.4) 16 | listen (>= 2.7, < 4.0) 17 | lumberjack (>= 1.0.12, < 2.0) 18 | nenv (~> 0.1) 19 | notiffany (~> 0.0) 20 | pry (>= 0.13.0) 21 | shellany (~> 0.0) 22 | thor (>= 0.18.1) 23 | guard-compat (1.2.1) 24 | guard-rake (1.0.0) 25 | guard 26 | rake 27 | guard-rspec (4.7.3) 28 | guard (~> 2.1) 29 | guard-compat (~> 1.1) 30 | rspec (>= 2.99.0, < 4.0) 31 | listen (3.7.1) 32 | rb-fsevent (~> 0.10, >= 0.10.3) 33 | rb-inotify (~> 0.9, >= 0.9.10) 34 | lumberjack (1.2.8) 35 | method_source (1.0.0) 36 | nenv (0.3.0) 37 | notiffany (0.1.3) 38 | nenv (~> 0.1) 39 | shellany (~> 0.0) 40 | oedipus_lex (2.6.0) 41 | parser (3.3.4.0) 42 | ast (~> 2.4.1) 43 | racc 44 | parser_node_ext (1.4.2) 45 | parser 46 | prettier_print (1.2.1) 47 | prism (1.3.0) 48 | prism_ext (0.4.2) 49 | prism 50 | pry (0.14.1) 51 | coderay (~> 1.1) 52 | method_source (~> 1.0) 53 | racc (1.8.0) 54 | rake (13.0.6) 55 | rb-fsevent (0.11.1) 56 | rb-inotify (0.10.1) 57 | ffi (~> 1.0) 58 | rspec (3.11.0) 59 | rspec-core (~> 3.11.0) 60 | rspec-expectations (~> 3.11.0) 61 | rspec-mocks (~> 3.11.0) 62 | rspec-core (3.11.0) 63 | rspec-support (~> 3.11.0) 64 | rspec-expectations (3.11.0) 65 | diff-lcs (>= 1.2.0, < 2.0) 66 | rspec-support (~> 3.11.0) 67 | rspec-mocks (3.11.1) 68 | diff-lcs (>= 1.2.0, < 2.0) 69 | rspec-support (~> 3.11.0) 70 | rspec-support (3.11.0) 71 | shellany (0.0.1) 72 | syntax_tree (6.2.0) 73 | prettier_print (>= 1.2.0) 74 | syntax_tree_ext (0.9.2) 75 | syntax_tree 76 | thor (1.2.1) 77 | 78 | PLATFORMS 79 | arm64-darwin-24 80 | x86_64-darwin-21 81 | x86_64-darwin-22 82 | x86_64-darwin-23 83 | x86_64-linux 84 | 85 | DEPENDENCIES 86 | guard 87 | guard-rake 88 | guard-rspec 89 | node_query! 90 | oedipus_lex 91 | parser_node_ext 92 | prism_ext 93 | racc 94 | rake (~> 13.0) 95 | rspec (~> 3.0) 96 | syntax_tree_ext 97 | 98 | BUNDLED WITH 99 | 2.4.22 100 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | guard :rspec, cmd: 'bundle exec rspec' do 4 | watch(%r{^spec/.+_spec\.rb$}) 5 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 6 | watch('lib/node_query_lexer.rex.rb') { 'spec/node_query_lexer_spec.rb' } 7 | watch('lib/node_query_compiler.rb') { 'spec/node_query_parser_spec.rb' } 8 | watch(%r{^lib/node_query/compiler/.*\.rb$}) { 'spec/node_query_parser_spec.rb' } 9 | watch('lib/node_query/parser.racc.rb') { 'spec/node_query_parser_spec.rb' } 10 | watch('spec/spec_helper.rb') { "spec" } 11 | end 12 | 13 | guard :rake, task: 'generate' do 14 | watch('lib/node_query_lexer.rex') 15 | watch('lib/node_query_parser.y') 16 | end 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NodeQuery 2 | 3 | [![Build Status](https://github.com/synvert-hq/node-query-ruby/actions/workflows/main.yml/badge.svg)](https://github.com/synvert-hq/node-query-ruby/actions/workflows/main.yml) 4 | [![Gem Version](https://img.shields.io/gem/v/node_query.svg)](https://rubygems.org/gems/node_query) 5 | 6 | NodeQuery defines a NQL (node query language) and node rules to query AST nodes. 7 | 8 | ## Table of Contents 9 | 10 | - [NodeQuery](#nodequery) 11 | - [Table of Contents](#table-of-contents) 12 | - [Installation](#installation) 13 | - [Usage](#usage) 14 | - [Node Query Language](#node-query-language) 15 | - [nql matches node type](#nql-matches-node-type) 16 | - [nql matches attribute](#nql-matches-attribute) 17 | - [nql matches nested attribute](#nql-matches-nested-attribute) 18 | - [nql matches evaluated value](#nql-matches-evaluated-value) 19 | - [nql matches nested selector](#nql-matches-nested-selector) 20 | - [nql matches method result](#nql-matches-method-result) 21 | - [nql matches operators](#nql-matches-operators) 22 | - [nql matches array node attribute](#nql-matches-array-node-attribute) 23 | - [nql matches \* in attribute key](#nql-matches--in-attribute-key) 24 | - [nql matches multiple selectors](#nql-matches-multiple-selectors) 25 | - [Descendant combinator](#descendant-combinator) 26 | - [Child combinator](#child-combinator) 27 | - [Adjacent sibling combinator](#adjacent-sibling-combinator) 28 | - [General sibling combinator](#general-sibling-combinator) 29 | - [nql matches goto scope](#nql-matches-goto-scope) 30 | - [nql matches :has and :not\_has pseudo selector](#nql-matches-has-and-not_has-pseudo-selector) 31 | - [nql matches :first-child and :last-child pseudo selector](#nql-matches-first-child-and-last-child-pseudo-selector) 32 | - [nql matches multiple expressions](#nql-matches-multiple-expressions) 33 | - [Node Rules](#node-rules) 34 | - [rules matches node type](#rules-matches-node-type) 35 | - [rules matches attribute](#rules-matches-attribute) 36 | - [rules matches nested attribute](#rules-matches-nested-attribute) 37 | - [rules matches evaluated value](#rules-matches-evaluated-value) 38 | - [rules matches nested selector](#rules-matches-nested-selector) 39 | - [rules matches method result](#rules-matches-method-result) 40 | - [rules matches operators](#rules-matches-operators) 41 | - [rules matches array nodes attribute](#rules-matches-array-nodes-attribute) 42 | - [Write Adapter](#write-adapter) 43 | - [Development](#development) 44 | - [Contributing](#contributing) 45 | 46 | ## Installation 47 | 48 | Add this line to your application's Gemfile: 49 | 50 | ```ruby 51 | gem 'node_query' 52 | ``` 53 | 54 | And then execute: 55 | 56 | $ bundle install 57 | 58 | Or install it yourself as: 59 | 60 | $ gem install node_query 61 | 62 | ## Usage 63 | 64 | It provides two apis: `query_nodes` and `match_node?` 65 | 66 | ```ruby 67 | node_query = NodeQuery.new(nql_or_rules: String | Hash, adapter: Symbol) # Initialize NodeQuery 68 | node_query.query_nodes(node: Node, options = { including_self: true, stop_at_first_match: false, recursive: true }): Node[] # Get the matching nodes. 69 | node_query.match_node?(node: Node): boolean # Check if the node matches nql or rules. 70 | ``` 71 | 72 | Here is an example for parser ast node. 73 | 74 | ```ruby 75 | source = ` 76 | class User 77 | def initialize(id, name) 78 | @id = id 79 | @name = name 80 | end 81 | end 82 | 83 | user = User.new(1, "Murphy") 84 | ` 85 | node = Parser::CurrentRuby.parse(source) 86 | 87 | # It will get the node of initialize. 88 | NodeQuery.new('.def[name=initialize]', adapter: :parser).query_nodes(node) 89 | NodeQuery.new({ node_type: 'def', name: 'initialize' }, adapter: :parser).query_nodes(node) 90 | ``` 91 | 92 | ## Node Query Language 93 | 94 | ### nql matches node type 95 | 96 | ``` 97 | .class 98 | ``` 99 | 100 | It matches class node 101 | 102 | ### nql matches attribute 103 | 104 | ``` 105 | .class[name=User] 106 | ``` 107 | 108 | It matches class node whose name is User 109 | 110 | ### nql matches nested attribute 111 | 112 | ``` 113 | .class[parent_class.name=Base] 114 | ``` 115 | 116 | It matches class node whose parent class name is Base 117 | 118 | ### nql matches evaluated value 119 | 120 | ``` 121 | .ivasgn[variable="@{{value}}"] 122 | ``` 123 | 124 | It matches ivasgn node whose left value equals '@' plus the evaluated value of right value. 125 | 126 | ### nql matches nested selector 127 | 128 | ``` 129 | .def[body.0=.ivasgn] 130 | ``` 131 | 132 | It matches def node whose first child node is an ivasgn node. 133 | 134 | ### nql matches method result 135 | 136 | ``` 137 | .def[arguments.size=2] 138 | ``` 139 | 140 | It matches def node whose arguments size is 2. 141 | 142 | ### nql matches operators 143 | 144 | ``` 145 | .class[name=User] 146 | ``` 147 | 148 | Value of name is equal to User 149 | 150 | ``` 151 | .class[name^=User] 152 | ``` 153 | 154 | Value of name starts with User 155 | 156 | ``` 157 | .class[name$=User] 158 | ``` 159 | 160 | Value of name ends with User 161 | 162 | ``` 163 | .class[name*=User] 164 | ``` 165 | 166 | Value of name contains User 167 | 168 | ``` 169 | .def[arguments.size!=2] 170 | ``` 171 | 172 | Size of arguments is not equal to 2 173 | 174 | ``` 175 | .def[arguments.size>=2] 176 | ``` 177 | 178 | Size of arguments is greater than or equal to 2 179 | 180 | ``` 181 | .def[arguments.size>2] 182 | ``` 183 | 184 | Size of arguments is greater than 2 185 | 186 | ``` 187 | .def[arguments.size<=2] 188 | ``` 189 | 190 | Size of arguments is less than or equal to 2 191 | 192 | ``` 193 | .def[arguments.size<2] 194 | ``` 195 | 196 | Size of arguments is less than 2 197 | 198 | ``` 199 | .class[name IN (User Account)] 200 | ``` 201 | 202 | Value of name is either User or Account 203 | 204 | ``` 205 | .class[name NOT IN (User Account)] 206 | ``` 207 | 208 | Value of name is neither User nor Account 209 | 210 | ``` 211 | .def[arguments INCLUDES id] 212 | ``` 213 | 214 | Value of arguments includes id 215 | 216 | ``` 217 | .def[arguments NOT INCLUDES id] 218 | ``` 219 | 220 | Value of arguments not includes id 221 | 222 | ``` 223 | .class[name=~/User/] 224 | ``` 225 | 226 | Value of name matches User 227 | 228 | ``` 229 | .class[name!~/User/] 230 | ``` 231 | 232 | Value of name does not match User 233 | 234 | ``` 235 | .class[name IN (/User/ /Account/)] 236 | ``` 237 | 238 | Value of name matches either /User/ or /Account/ 239 | 240 | ### nql matches array node attribute 241 | 242 | ``` 243 | .def[arguments=(id name)] 244 | ``` 245 | 246 | It matches def node whose arguments are id and name. 247 | 248 | ### nql matches * in attribute key 249 | 250 | ``` 251 | .def[arguments.*.name IN (id name)] 252 | ``` 253 | 254 | It matches def node whose arguments are either id or name. 255 | 256 | ### nql matches multiple selectors 257 | 258 | #### Descendant combinator 259 | 260 | ``` 261 | .class .send 262 | ``` 263 | 264 | It matches send node whose ancestor is class node. 265 | 266 | #### Child combinator 267 | 268 | ``` 269 | .def > .send 270 | ``` 271 | 272 | It matches send node whose parent is def node. 273 | 274 | #### Adjacent sibling combinator 275 | 276 | ``` 277 | .send[variable=@id] + .send 278 | ``` 279 | 280 | It matches send node only if it is immediately follows the send node whose left value is @id. 281 | 282 | #### General sibling combinator 283 | 284 | ``` 285 | .send[variable=@id] ~ .send 286 | ``` 287 | 288 | It matches send node only if it is follows the send node whose left value is @id. 289 | 290 | ### nql matches goto scope 291 | 292 | ``` 293 | .def body .send 294 | ``` 295 | 296 | It matches send node who is in the body of def node. 297 | 298 | ### nql matches :has and :not_has pseudo selector 299 | 300 | ``` 301 | .class:has(.def[name=initialize]) 302 | ``` 303 | 304 | It matches class node who has an initialize def node. 305 | 306 | ``` 307 | .class:not_has(.def[name=initialize]) 308 | ``` 309 | 310 | It matches class node who does not have an initialize def node. 311 | 312 | ### nql matches :first-child and :last-child pseudo selector 313 | 314 | ``` 315 | .def:first-child 316 | ``` 317 | 318 | It matches the first def node. 319 | 320 | ``` 321 | .def:last-child 322 | ``` 323 | 324 | It matches the last def node. 325 | 326 | ### nql matches multiple expressions 327 | 328 | ``` 329 | .ivasgn[variable=@id], .ivasgn[variable=@name] 330 | ``` 331 | 332 | It matches ivasgn node whose left value is either @id or @name. 333 | 334 | ## Node Rules 335 | 336 | ### rules matches node type 337 | 338 | ``` 339 | { node_type: 'class' } 340 | ``` 341 | 342 | It matches class node 343 | 344 | ### rules matches attribute 345 | 346 | ``` 347 | { node_type: 'def', name: 'initialize' } 348 | ``` 349 | 350 | It matches def node whose name is initialize 351 | 352 | ``` 353 | { node_type: 'def', arguments: { "0": 1, "1": "Murphy" } } 354 | ``` 355 | 356 | It matches def node whose arguments are 1 and Murphy. 357 | 358 | ### rules matches nested attribute 359 | 360 | ``` 361 | { node_type: 'class', parent_class: { name: 'Base' } } 362 | ``` 363 | 364 | It matches class node whose parent class name is Base 365 | 366 | ### rules matches evaluated value 367 | 368 | ``` 369 | { node_type: 'ivasgn', variable: '@{{value}}' } 370 | ``` 371 | 372 | It matches ivasgn node whose left value equals '@' plus the evaluated value of right value. 373 | 374 | ### rules matches nested selector 375 | 376 | ``` 377 | { node_type: 'def', body: { "0": { node_type: 'ivasgn' } } } 378 | ``` 379 | 380 | It matches def node whose first child node is an ivasgn node. 381 | 382 | ### rules matches method result 383 | 384 | ``` 385 | { node_type: 'def', arguments: { size: 2 } } 386 | ``` 387 | 388 | It matches def node whose arguments size is 2. 389 | 390 | ### rules matches operators 391 | 392 | ``` 393 | { node_type: 'class', name: 'User' } 394 | ``` 395 | 396 | Value of name is equal to User 397 | 398 | ``` 399 | { node_type: 'def', arguments: { size { not: 2 } } 400 | ``` 401 | 402 | Size of arguments is not equal to 2 403 | 404 | ``` 405 | { node_type: 'def', arguments: { size { gte: 2 } } 406 | ``` 407 | 408 | Size of arguments is greater than or equal to 2 409 | 410 | ``` 411 | { node_type: 'def', arguments: { size { gt: 2 } } 412 | ``` 413 | 414 | Size of arguments is greater than 2 415 | 416 | ``` 417 | { node_type: 'def', arguments: { size { lte: 2 } } 418 | ``` 419 | 420 | Size of arguments is less than or equal to 2 421 | 422 | ``` 423 | { node_type: 'def', arguments: { size { lt: 2 } } 424 | ``` 425 | 426 | Size of arguments is less than 2 427 | 428 | ``` 429 | { node_type: 'class', name: { in: ['User', 'Account'] } } 430 | ``` 431 | 432 | Value of name is either User or Account 433 | 434 | ``` 435 | { node_type: 'class', name: { not_in: ['User', 'Account'] } } 436 | ``` 437 | 438 | Value of name is neither User nor Account 439 | 440 | ``` 441 | { node_type: 'def', arguments: { includes: 'id' } } 442 | ``` 443 | 444 | Value of arguments includes id 445 | 446 | ``` 447 | { node_type: 'def', arguments: { not_includes: 'id' } } 448 | ``` 449 | 450 | Value of arguments not includes id 451 | 452 | ``` 453 | { node_type: 'class', name: /User/ } 454 | ``` 455 | 456 | Value of name matches User 457 | 458 | ``` 459 | { node_type: 'class', name: { not: /User/ } } 460 | ``` 461 | 462 | Value of name does not match User 463 | 464 | ``` 465 | { node_type: 'class', name: { in: [/User/, /Account/] } } 466 | ``` 467 | 468 | Value of name matches either /User/ or /Account/ 469 | 470 | ### rules matches array nodes attribute 471 | 472 | ``` 473 | { node_type: 'def', arguments: ['id', 'name'] } 474 | ``` 475 | 476 | It matches def node whose arguments are id and name. 477 | 478 | ## Write Adapter 479 | 480 | Different parser, like prism, parser, syntax_tree, will generate different AST nodes, to make NodeQuery work for them all, 481 | we define an [Adapter](https://github.com/synvert-hq/node-query-ruby/blob/main/lib/node_query/adapter.rb) interface, 482 | if you implement the Adapter interface, you can set it as NodeQuery's adapter. 483 | 484 | It provides 3 adapters 485 | 486 | 1. `PrismAdapter` 487 | 2. `ParserAdapter` 488 | 3. `SyntaxTreeAdapter` 489 | 490 | ## Development 491 | 492 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 493 | 494 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). 495 | 496 | ## Contributing 497 | 498 | Bug reports and pull requests are welcome on GitHub at https://github.com/synvert-hq/node-query-ruby. 499 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require 'rspec/core/rake_task' 5 | RSpec::Core::RakeTask.new(:spec) 6 | require 'oedipus_lex' 7 | Rake.application.rake_require "oedipus_lex" 8 | 9 | file "lib/node_query_lexer.rex.rb" => "lib/node_query_lexer.rex" 10 | file "lib/node_query_parser.racc.rb" => "lib/node_query_parser.y" 11 | 12 | task :lexer => "lib/node_query_lexer.rex.rb" 13 | task :parser => "lib/node_query_parser.racc.rb" 14 | task :generate => [:lexer, :parser] 15 | 16 | rule '.racc.rb' => '.y' do |t| 17 | cmd = "bundle exec racc -l -v -o #{t.name} #{t.source}" 18 | sh cmd 19 | end 20 | 21 | task :default => :spec 22 | task :spec => :generate 23 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "node_query" 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require "irb" 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/node_query.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "node_query/version" 4 | require_relative "./node_query_lexer.rex" 5 | require_relative "./node_query_parser.racc" 6 | 7 | class NodeQuery 8 | class MethodNotSupported < StandardError; end 9 | class InvalidAdapterError < StandardError; end 10 | 11 | autoload :Adapter, "node_query/adapter" 12 | autoload :ParserAdapter, "node_query/adapter/parser" 13 | autoload :SyntaxTreeAdapter, "node_query/adapter/syntax_tree" 14 | autoload :PrismAdapter, "node_query/adapter/prism" 15 | autoload :Compiler, "node_query/compiler" 16 | autoload :Helper, "node_query/helper" 17 | autoload :NodeRules, "node_query/node_rules" 18 | 19 | # Initialize a NodeQuery. 20 | # @param nql_or_ruls [String | Hash] node query language or node rules 21 | # @param adapter [Symbol] :parser or :syntax_tree 22 | def initialize(nql_or_ruls, adapter: :parser) 23 | adapter_instance = get_adapter_instance(adapter) 24 | if nql_or_ruls.is_a?(String) 25 | @expression = NodeQueryParser.new(adapter: adapter_instance).parse(nql_or_ruls) 26 | else 27 | @rules = NodeRules.new(nql_or_ruls, adapter: adapter_instance) 28 | end 29 | end 30 | 31 | # Query matching nodes. 32 | # @param node [Node] ast node 33 | # @param options [Hash] if query the current node 34 | # @option options [boolean] :including_self if query the current node, default is ture 35 | # @option options [boolean] :stop_at_first_match if stop at first match, default is false 36 | # @option options [boolean] :recursive if recursively query child nodes, default is true 37 | # @return [Array] matching child nodes 38 | def query_nodes(node, options = {}) 39 | if @expression 40 | @expression.query_nodes(node, options) 41 | elsif @rules 42 | @rules.query_nodes(node, options) 43 | else 44 | [] 45 | end 46 | end 47 | 48 | # Check if the node matches the nql or rules. 49 | # @param node [Node] the node 50 | # @return [Boolean] 51 | def match_node?(node) 52 | if @expression 53 | @expression.match_node?(node) 54 | elsif @rules 55 | @rules.match_node?(node) 56 | else 57 | false 58 | end 59 | end 60 | 61 | private 62 | 63 | def get_adapter_instance(adapter) 64 | case adapter.to_sym 65 | when :parser 66 | ParserAdapter.new 67 | when :syntax_tree 68 | SyntaxTreeAdapter.new 69 | when :prism 70 | PrismAdapter.new 71 | else 72 | raise InvalidAdapterError, "adapter #{adapter} is not supported" 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/node_query/adapter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Abstract Adapter class 4 | class NodeQuery::Adapter 5 | # Check if it is a node 6 | # @return [Boolean] 7 | def is_node?(node) 8 | raise NotImplementedError, 'get_node_type is not implemented' 9 | end 10 | 11 | # Get the type of node 12 | # @param node [Node] ast node 13 | # @return [Symbol] node type 14 | def get_node_type(node) 15 | raise NotImplementedError, 'get_node_type is not implemented' 16 | end 17 | 18 | # Get the source code of node 19 | # @param node [Node] ast node 20 | # @return [String] node source code 21 | def get_source(node) 22 | raise NotImplementedError, 'get_source is not implemented' 23 | end 24 | 25 | # Get the children of node 26 | # @param node [Node] ast node 27 | # @return [Array] node children 28 | def get_children(node) 29 | raise NotImplementedError, 'get_children is not implemented' 30 | end 31 | 32 | # Get the siblings of node 33 | # @param node [Node] ast node 34 | # @return [Array] node siblings 35 | def get_siblings(node) 36 | raise NotImplementedError, 'get_siblings is not implemented' 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/node_query/adapter/parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'parser' 4 | require 'parser_node_ext' 5 | 6 | class NodeQuery::ParserAdapter 7 | def is_node?(node) 8 | node.is_a?(Parser::AST::Node) 9 | end 10 | 11 | def get_node_type(node) 12 | node.type 13 | end 14 | 15 | def get_source(node) 16 | node.loc.expression.source 17 | end 18 | 19 | def get_children(node) 20 | node.children 21 | end 22 | 23 | def get_siblings(node) 24 | index = node.parent.children.index(node) 25 | node.parent.children[index + 1..] 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/node_query/adapter/prism.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'prism' 4 | require 'prism_ext' 5 | 6 | class NodeQuery::PrismAdapter 7 | def is_node?(node) 8 | node.is_a?(Prism::Node) 9 | end 10 | 11 | def get_node_type(node) 12 | node.type 13 | end 14 | 15 | def get_source(node) 16 | node.to_source 17 | end 18 | 19 | def get_children(node) 20 | keys = [] 21 | children = [] 22 | node.deconstruct_keys([]).each do |key, value| 23 | next if [:node_id, :flags, :location].include?(key) 24 | 25 | if key.to_s.end_with?('_loc') 26 | new_key = key.to_s[0..-5] 27 | unless keys.include?(new_key) 28 | keys << new_key 29 | children << node.send(new_key) 30 | end 31 | else 32 | unless keys.include?(key.to_s) 33 | keys << key.to_s 34 | children << value 35 | end 36 | end 37 | end 38 | children 39 | end 40 | 41 | def get_siblings(node) 42 | child_nodes = get_children(node.parent_node) 43 | if child_nodes.is_a?(Array) && child_nodes.size == 1 && child_nodes.first.is_a?(Array) 44 | index = child_nodes.first.index(node) 45 | return child_nodes.first[index + 1...] 46 | end 47 | 48 | index = child_nodes.index(node) 49 | child_nodes[index + 1...] 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/node_query/adapter/syntax_tree.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'syntax_tree' 4 | require 'syntax_tree_ext' 5 | 6 | class NodeQuery::SyntaxTreeAdapter 7 | def is_node?(node) 8 | node.is_a?(SyntaxTree::Node) 9 | end 10 | 11 | def get_node_type(node) 12 | node.class.name.split('::').last.to_sym 13 | end 14 | 15 | def get_source(node) 16 | node.to_source 17 | end 18 | 19 | def get_children(node) 20 | node.deconstruct_keys([]).filter { |key, _value| ![:location, :comments].include?(key) } 21 | .values 22 | end 23 | 24 | def get_siblings(node) 25 | child_nodes = get_children(node.parent_node) 26 | if child_nodes.is_a?(Array) && child_nodes.size == 1 && child_nodes.first.is_a?(Array) 27 | index = child_nodes.first.index(node) 28 | return child_nodes.first[index + 1...] 29 | end 30 | 31 | index = child_nodes.index(node) 32 | child_nodes[index + 1...] 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/node_query/compiler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module NodeQuery::Compiler 4 | autoload :InvalidOperatorError, 'node_query/compiler/invalid_operator_error' 5 | autoload :ParseError, 'node_query/compiler/parse_error' 6 | 7 | autoload :Comparable, 'node_query/compiler/comparable' 8 | 9 | autoload :ExpressionList, 'node_query/compiler/expression_list' 10 | autoload :Expression, 'node_query/compiler/expression' 11 | autoload :Selector, 'node_query/compiler/selector' 12 | autoload :BasicSelector, 'node_query/compiler/basic_selector' 13 | autoload :AttributeList, 'node_query/compiler/attribute_list' 14 | autoload :Attribute, 'node_query/compiler/attribute' 15 | 16 | autoload :ArrayValue, 'node_query/compiler/array_value' 17 | autoload :Boolean, 'node_query/compiler/boolean' 18 | autoload :Float, 'node_query/compiler/float' 19 | autoload :Identifier, 'node_query/compiler/identifier' 20 | autoload :Integer, 'node_query/compiler/integer' 21 | autoload :Nil, 'node_query/compiler/nil' 22 | autoload :Regexp, 'node_query/compiler/regexp' 23 | autoload :String, 'node_query/compiler/string' 24 | autoload :Symbol, 'node_query/compiler/symbol' 25 | end 26 | -------------------------------------------------------------------------------- /lib/node_query/compiler/array_value.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module NodeQuery::Compiler 4 | # ArrayValue represents a ruby array value. 5 | class ArrayValue 6 | include Comparable 7 | 8 | # Initialize an Array. 9 | # @param value the first value of the array 10 | # @param rest the rest value of the array 11 | # @param adapter [NodeQuery::Adapter] 12 | def initialize(value: nil, rest: nil, adapter:) 13 | @value = value 14 | @rest = rest 15 | @adapter = adapter 16 | end 17 | 18 | # Get the expected value. 19 | # @return [Array] 20 | def expected_value(base_node) 21 | expected = [] 22 | expected.push(@value) if @value 23 | expected += @rest.expected_value(base_node) if @rest 24 | expected 25 | end 26 | 27 | # Get valid operators. 28 | # @return [Array] valid operators 29 | def valid_operators 30 | ARRAY_VALID_OPERATORS 31 | end 32 | 33 | def to_s 34 | [@value, @rest].compact.join(' ') 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/node_query/compiler/attribute.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module NodeQuery::Compiler 4 | # Attribute is a pair of key, value and operator, 5 | class Attribute 6 | # Initialize a Attribute. 7 | # @param key [String] the key 8 | # @param value the value can be any class implement {NodeQuery::Compiler::Comparable} 9 | # @param operator [String] the operator 10 | # @param adapter [NodeQuery::Adapter] 11 | def initialize(key:, value:, operator: '=', adapter:) 12 | @key = key 13 | @value = value 14 | @operator = operator 15 | @adapter = adapter 16 | end 17 | 18 | # Check if the node matches the attribute. 19 | # @param node [Node] the node 20 | # @param base_node [Node] the bae node for evaluated value 21 | # @return [Boolean] 22 | def match?(node, base_node) 23 | node && @value.match?(NodeQuery::Helper.get_target_node(node, @key, @adapter), base_node, @operator) 24 | end 25 | 26 | def to_s 27 | case @operator 28 | when '^=', '$=', '*=', '!=', '=~', '!~', '>=', '>', '<=', '<' 29 | "#{@key}#{@operator}#{@value}" 30 | when 'in' 31 | "#{@key} in (#{@value})" 32 | when 'not_in' 33 | "#{@key} not in (#{@value})" 34 | when 'includes' 35 | "#{@key} includes #{@value}" 36 | when 'not_includes' 37 | "#{@key} not includes #{@value}" 38 | else 39 | "#{@key}=#{@value}" 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/node_query/compiler/attribute_list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module NodeQuery::Compiler 4 | # AttributeList contains one or more {NodeQuery::Compiler::Attribute}. 5 | class AttributeList 6 | # Initialize an AttributeList. 7 | # @param attribute [NodeQuery::Compiler::Attribute] the attribute 8 | # @param rest [NodeQuery::Compiler::AttributeList] the rest attribute list 9 | def initialize(attribute:, rest: nil) 10 | @attribute = attribute 11 | @rest = rest 12 | end 13 | 14 | # Check if the node matches the attribute list. 15 | # @param node [Node] the node 16 | # @param base_node [Node] the base node for evaluated value 17 | # @return [Boolean] 18 | def match?(node, base_node) 19 | @attribute.match?(node, base_node) && (!@rest || @rest.match?(node, base_node)) 20 | end 21 | 22 | def to_s 23 | "[#{@attribute}]#{@rest}" 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/node_query/compiler/basic_selector.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module NodeQuery::Compiler 4 | # BasicSelector used to match nodes, it combines by node type and/or attribute list. 5 | class BasicSelector 6 | # Initialize a BasicSelector. 7 | # @param node_type [String] the node type 8 | # @param attribute_list [NodeQuery::Compiler::AttributeList] the attribute list 9 | # @param adapter [NodeQuery::Adapter] 10 | def initialize(node_type:, attribute_list: nil, adapter:) 11 | @node_type = node_type 12 | @attribute_list = attribute_list 13 | @adapter = adapter 14 | end 15 | 16 | # Check if node matches the selector. 17 | # @param node [Node] the node 18 | # @param base_node [Node] the base node for evaluated value 19 | # @return [Boolean] 20 | def match?(node, base_node, _operator = '=') 21 | return false unless node 22 | 23 | @node_type.to_sym == @adapter.get_node_type(node) && (!@attribute_list || @attribute_list.match?( 24 | node, 25 | base_node 26 | )) 27 | end 28 | 29 | def to_s 30 | result = [".#{@node_type}"] 31 | result << @attribute_list.to_s if @attribute_list 32 | result.join('') 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/node_query/compiler/boolean.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module NodeQuery::Compiler 4 | # Boolean represents a ruby boolean value. 5 | class Boolean 6 | include Comparable 7 | 8 | # Initialize a Boolean. 9 | # @param value [Boolean] the boolean value 10 | # @param adapter [NodeQuery::Adapter] 11 | def initialize(value:, adapter:) 12 | @value = value 13 | @adapter = adapter 14 | end 15 | 16 | # Get valid operators. 17 | # @return [Array] valid operators 18 | def valid_operators 19 | SIMPLE_VALID_OPERATORS 20 | end 21 | 22 | def to_s 23 | @value.to_s 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/node_query/compiler/comparable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module NodeQuery::Compiler 4 | # Compare acutal value with expected value. 5 | module Comparable 6 | SIMPLE_VALID_OPERATORS = ['=', '!=', 'includes', 'not_includes'] 7 | STRING_VALID_OPERATORS = ['=', '!=', '^=', '$=', '*=', 'includes', 'not_includes'] 8 | NUMBER_VALID_OPERATORS = ['=', '!=', '>', '>=', '<', '<=', 'includes', 'not_includes'] 9 | ARRAY_VALID_OPERATORS = ['=', '!=', 'in', 'not_in'] 10 | REGEXP_VALID_OPERATORS = ['=~', '!~'] 11 | 12 | # Check if the actual value matches the expected value. 13 | # 14 | # @param node [Node] node to calculate actual value 15 | # @param base_node [Node] the base node for evaluated value 16 | # @param operator [String] operator to compare with expected value, operator can be '=', '!=', '>', '>=', '<', '<=', 'includes', 'in', 'not_in', '=~', '!~' 17 | # @return [Boolean] true if actual value matches the expected value 18 | # @raise [NodeQuery::Compiler::InvalidOperatorError] if operator is invalid 19 | # @param adapter [NodeQuery::Adapter] 20 | def match?(node, base_node, operator) 21 | raise InvalidOperatorError, "invalid operator #{operator}" unless valid_operator?(operator) 22 | 23 | actual = actual_value(node) 24 | expected = expected_value(base_node) 25 | case operator 26 | when '!=' 27 | if expected.is_a?(::Array) 28 | !actual.is_a?(::Array) || actual.size != expected.size || 29 | actual.zip(expected).any? { |actual_child, expected_child| 30 | expected_child.match?(actual_child, base_node, '!=') 31 | } 32 | else 33 | !is_equal?(actual, expected) 34 | end 35 | when '=~' 36 | actual =~ expected 37 | when '!~' 38 | actual !~ expected 39 | when '^=' 40 | actual.start_with?(expected) 41 | when '$=' 42 | actual.end_with?(expected) 43 | when '*=' 44 | actual.include?(expected) 45 | when '>' 46 | actual > expected 47 | when '>=' 48 | actual >= expected 49 | when '<' 50 | actual < expected 51 | when '<=' 52 | actual <= expected 53 | when 'in' 54 | if node.is_a?(Array) 55 | node.all? { |child| expected.any? { |expected_child| expected_child.match?(child, base_node, '=') } } 56 | else 57 | expected.any? { |expected_child| expected_child.match?(node, base_node, '=') } 58 | end 59 | when 'not_in' 60 | if node.is_a?(Array) 61 | node.all? { |child| expected.all? { |expected_child| expected_child.match?(child, base_node, '!=') } } 62 | else 63 | expected.all? { |expected_child| expected_child.match?(node, base_node, '!=') } 64 | end 65 | when 'includes' 66 | actual.any? { |actual_child| expected.match?(actual_child) } 67 | when 'not_includes' 68 | actual.none? { |actual_child| expected.match?(actual_child) } 69 | else 70 | if expected.is_a?(::Array) 71 | actual.is_a?(::Array) && actual.size == expected.size && 72 | actual.zip(expected).all? { |actual_child, expected_child| 73 | expected_child.match?(actual_child, base_node, '=') 74 | } 75 | else 76 | is_equal?(actual, expected) 77 | end 78 | end 79 | end 80 | 81 | # Check if the actual value equals the expected value. 82 | # @param acutal 83 | # @param expected 84 | # @return [Boolean] true if the actual value equals the expected value. 85 | def is_equal?(actual, expected) 86 | actual == expected 87 | end 88 | 89 | # Get the actual value from ast node. 90 | # @param node [Node] ast node 91 | # @return the node value, could be integer, float, string, boolean, nil, range, and etc. 92 | def actual_value(node) 93 | if @adapter.is_node?(node) 94 | node.to_value 95 | else 96 | node 97 | end 98 | end 99 | 100 | # Get the expected value 101 | # @param base_node [Node] the base node for evaluated value 102 | # @return expected value, could be integer, float, string, boolean, nil, range, and etc. 103 | def expected_value(_base_node) 104 | @value 105 | end 106 | 107 | # Check if the operator is valid. 108 | # @return [Boolean] true if the operator is valid 109 | def valid_operator?(operator) 110 | valid_operators.include?(operator) 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /lib/node_query/compiler/expression.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module NodeQuery::Compiler 4 | # Expression represents a node query expression. 5 | class Expression 6 | # Initialize a Expression. 7 | # @param selector [NodeQuery::Compiler::Selector] the selector 8 | # @param rest [NodeQuery::Compiler::Expression] the rest expression 9 | def initialize(selector: nil, rest: nil) 10 | @selector = selector 11 | @rest = rest 12 | end 13 | 14 | # Query nodes by the selector and the rest expression. 15 | # @param node [Node] node to match 16 | # @param options [Hash] if query the current node 17 | # @option options [boolean] :including_self if query the current node, default is ture 18 | # @option options [boolean] :stop_at_first_match if stop at first match, default is false 19 | # @option options [boolean] :recursive if recursively query child nodes, default is true 20 | # @return [Array] matching nodes. 21 | def query_nodes(node, options = {}) 22 | matching_nodes = @selector.query_nodes(node, options) 23 | return matching_nodes if @rest.nil? 24 | 25 | matching_nodes.flat_map do |matching_node| 26 | @rest.query_nodes(matching_node, options) 27 | end 28 | end 29 | 30 | def to_s 31 | result = [] 32 | result << @selector.to_s if @selector 33 | result << @rest.to_s if @rest 34 | result.join(' ') 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/node_query/compiler/expression_list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module NodeQuery::Compiler 4 | # ExpressionList contains one or more {NodeQuery::Compiler::Expression}. 5 | class ExpressionList 6 | # Initialize an ExpressionList. 7 | # @param expression [NodeQuery::Compiler::Expression] the expression 8 | # @param rest [NodeQuery::Compiler::ExpressionList] the rest expression list 9 | def initialize(expression:, rest: nil) 10 | @expression = expression 11 | @rest = rest 12 | end 13 | 14 | # Query nodes by the current and the rest expression. 15 | # @param node [Node] node to match 16 | # @param options [Hash] if query the current node 17 | # @option options [boolean] :including_self if query the current node, default is ture 18 | # @option options [boolean] :stop_at_first_match if stop at first match, default is false 19 | # @option options [boolean] :recursive if recursively query child nodes, default is true 20 | # @return [Array] matching nodes. 21 | def query_nodes(node, options = {}) 22 | matching_nodes = @expression.query_nodes(node, options) 23 | return matching_nodes if @rest.nil? 24 | 25 | matching_nodes + @rest.query_nodes(node, options) 26 | end 27 | 28 | # Check if the node matches the expression list. 29 | # @param node [Node] the node 30 | # @return [Boolean] 31 | def match_node?(node) 32 | !query_nodes(node).empty? 33 | end 34 | 35 | def to_s 36 | [@expression, @rest].compact.join(', ') 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/node_query/compiler/float.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module NodeQuery::Compiler 4 | # Float represents a ruby float value. 5 | class Float 6 | include Comparable 7 | 8 | # Initialize a Float. 9 | # @param value [Float] the float value 10 | # @param adapter [NodeQuery::Adapter] 11 | def initialize(value:, adapter:) 12 | @value = value 13 | @adapter = adapter 14 | end 15 | 16 | # Get valid operators. 17 | # @return [Array] valid operators 18 | def valid_operators 19 | NUMBER_VALID_OPERATORS 20 | end 21 | 22 | def to_s 23 | @value.to_s 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/node_query/compiler/identifier.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module NodeQuery::Compiler 4 | # Identifier represents a ruby identifier value. 5 | # e.g. code is `class Synvert; end`, `Synvert` is an identifier. 6 | class Identifier 7 | include Comparable 8 | 9 | # Initialize an Identifier. 10 | # @param value [String] the identifier value 11 | # @param adapter [NodeQuery::Adapter] 12 | def initialize(value:, adapter:) 13 | @value = value 14 | @adapter = adapter 15 | end 16 | 17 | # Get the actual value. 18 | # @param node [Node] the node 19 | # @return [String|Array] 20 | # If the node is a {Node}, return the node source code, 21 | # if the node is an Array, return the array of each element's actual value, 22 | # otherwise, return the String value. 23 | def actual_value(node) 24 | if @adapter.is_node?(node) 25 | @adapter.get_source(node) 26 | elsif node.is_a?(::Array) 27 | node.map { |n| actual_value(n) } 28 | else 29 | node.to_s 30 | end 31 | end 32 | 33 | # Get valid operators. 34 | # @return [Array] valid operators 35 | def valid_operators 36 | STRING_VALID_OPERATORS 37 | end 38 | 39 | def to_s 40 | @value 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/node_query/compiler/integer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module NodeQuery::Compiler 4 | # Integer represents a ruby integer value. 5 | class Integer 6 | include Comparable 7 | 8 | # Initialize a Integer. 9 | # @param value [Integer] the integer value 10 | # @param adapter [NodeQuery::Adapter] 11 | def initialize(value:, adapter:) 12 | @value = value 13 | @adapter = adapter 14 | end 15 | 16 | # Get valid operators. 17 | # @return [Array] valid operators 18 | def valid_operators 19 | NUMBER_VALID_OPERATORS 20 | end 21 | 22 | def to_s 23 | @value.to_s 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/node_query/compiler/invalid_operator_error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module NodeQuery::Compiler 4 | # Operator is invalid error. 5 | class InvalidOperatorError < StandardError 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/node_query/compiler/nil.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module NodeQuery::Compiler 4 | # Nil represents a ruby nil value. 5 | class Nil 6 | include Comparable 7 | 8 | # Initialize a Nil. 9 | # @param value [nil] the nil value 10 | # @param adapter [NodeQuery::Adapter] 11 | def initialize(value:, adapter:) 12 | @value = value 13 | @adapter = adapter 14 | end 15 | 16 | # Get valid operators. 17 | # @return [Array] valid operators 18 | def valid_operators 19 | SIMPLE_VALID_OPERATORS 20 | end 21 | 22 | def to_s 23 | 'nil' 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/node_query/compiler/parse_error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module NodeQuery::Compiler 4 | # The parser error. 5 | class ParseError < StandardError 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/node_query/compiler/regexp.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module NodeQuery::Compiler 4 | # Regexp represents a ruby regexp value. 5 | class Regexp 6 | include Comparable 7 | 8 | # Initialize a Regexp. 9 | # @param value [Regexp] the regexp value 10 | # @param adapter [NodeQuery::Adapter] 11 | def initialize(value:, adapter:) 12 | @value = value 13 | @adapter = adapter 14 | end 15 | 16 | # Check if the regexp value matches the node value. 17 | # @param node [Node] the node 18 | # @param _base_node [Node] the base node for evaluated value 19 | # @param operator [String] the operator 20 | # @return [Boolean] true if the regexp value matches the node value, otherwise, false. 21 | def match?(node, _base_node, operator = '=~') 22 | match = 23 | if @adapter.is_node?(node) 24 | @value.match(@adapter.get_source(node)) 25 | else 26 | @value.match(node.to_s) 27 | end 28 | operator == '=~' ? match : !match 29 | end 30 | 31 | # Get valid operators. 32 | # @return [Array] valid operators 33 | def valid_operators 34 | REGEXP_VALID_OPERATORS 35 | end 36 | 37 | def to_s 38 | @value.to_s 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/node_query/compiler/selector.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module NodeQuery::Compiler 4 | # Selector used to match nodes, it combines by node type and/or attribute list, plus index or has expression. 5 | class Selector 6 | # Initialize a Selector. 7 | # @param goto_scope [String] goto scope 8 | # @param relationship [String] the relationship between the selectors, it can be descendant nil, child >, next sibling + or subsequent sibing ~. 9 | # @param rest [NodeQuery::Compiler::Selector] the rest selector 10 | # @param basic_selector [NodeQuery::Compiler::BasicSelector] the simple selector 11 | # @param position [String] the position of the node 12 | # @param attribute_list [NodeQuery::Compiler::AttributeList] the attribute list 13 | # @param pseudo_class [String] the pseudo class, can be has or not_has 14 | # @param pseudo_selector [NodeQuery::Compiler::Expression] the pseudo selector 15 | # @param adapter [NodeQuery::Adapter] 16 | def initialize( 17 | goto_scope: nil, 18 | relationship: nil, 19 | rest: nil, 20 | basic_selector: nil, 21 | position: nil, 22 | pseudo_class: nil, 23 | pseudo_selector: nil, 24 | adapter: 25 | ) 26 | @goto_scope = goto_scope 27 | @relationship = relationship 28 | @rest = rest 29 | @basic_selector = basic_selector 30 | @position = position 31 | @pseudo_class = pseudo_class 32 | @pseudo_selector = pseudo_selector 33 | @adapter = adapter 34 | end 35 | 36 | # Check if node matches the selector. 37 | # @param node [Node] the node 38 | # @param base_node [Node] the base node for evaluated node 39 | def match?(node, base_node, operator = "=") 40 | if node.is_a?(::Array) 41 | case operator 42 | when "not_includes" 43 | return node.none? { |child_node| match?(child_node, base_node) } 44 | when "includes" 45 | return node.any? { |child_node| match?(child_node, base_node) } 46 | else 47 | return false 48 | end 49 | end 50 | @adapter.is_node?(node) && (!@basic_selector || (operator == "!=" ? !@basic_selector.match?( 51 | node, 52 | base_node 53 | ) : @basic_selector.match?(node, base_node))) && match_pseudo_class?(node) 54 | end 55 | 56 | # Query nodes by the selector. 57 | # * If relationship is nil, it will match in all recursive child nodes and return matching nodes. 58 | # * If relationship is decendant, it will match in all recursive child nodes. 59 | # * If relationship is child, it will match in direct child nodes. 60 | # * If relationship is next sibling, it try to match next sibling node. 61 | # * If relationship is subsequent sibling, it will match in all sibling nodes. 62 | # @param node [Node] node to match 63 | # @param options [Hash] if query the current node 64 | # @option options [boolean] :including_self if query the current node, default is ture 65 | # @option options [boolean] :stop_at_first_match if stop at first match, default is false 66 | # @option options [boolean] :recursive if recursively query child nodes, default is true 67 | # @return [Array] matching nodes. 68 | def query_nodes(node, options = {}) 69 | options = { including_self: true, stop_at_first_match: false, recursive: true }.merge(options) 70 | return find_nodes_by_relationship(node) if @relationship 71 | 72 | if node.is_a?(::Array) 73 | return node.flat_map { |child_node| query_nodes(child_node) } 74 | end 75 | 76 | return find_nodes_by_goto_scope(node) if @goto_scope 77 | 78 | if options[:including_self] && !options[:recursive] 79 | return match?(node, node) ? [node] : [] 80 | end 81 | 82 | nodes = [] 83 | if options[:including_self] && match?(node, node) 84 | nodes << node 85 | return nodes if options[:stop_at_first_match] 86 | end 87 | if @basic_selector 88 | if options[:recursive] 89 | NodeQuery::Helper.handle_recursive_child(node, @adapter) do |child_node| 90 | if match?(child_node, child_node) 91 | nodes << child_node 92 | break if options[:stop_at_first_match] 93 | end 94 | end 95 | else 96 | @adapter.get_children(node).each do |child_node| 97 | if match?(child_node, child_node) 98 | nodes << child_node 99 | break if options[:stop_at_first_match] 100 | end 101 | end 102 | end 103 | end 104 | filter_by_position(nodes) 105 | end 106 | 107 | def to_s 108 | result = [] 109 | result << "#{@goto_scope} " if @goto_scope 110 | result << "#{@relationship} " if @relationship 111 | result << @rest.to_s if @rest 112 | result << @basic_selector.to_s if @basic_selector 113 | result << ":#{@position}" if @position 114 | result << ":#{@pseudo_class}(#{@pseudo_selector})" if @pseudo_class 115 | result.join('') 116 | end 117 | 118 | protected 119 | 120 | # Filter nodes by position. 121 | # @param nodes [Array] nodes to filter 122 | # @return [Array|Node] first node or last node or nodes 123 | def filter_by_position(nodes) 124 | return nodes unless @position 125 | return nodes if nodes.empty? 126 | 127 | case @position 128 | when 'first-child' 129 | [nodes.first] 130 | when 'last-child' 131 | [nodes.last] 132 | else 133 | nodes 134 | end 135 | end 136 | 137 | private 138 | 139 | # Find nodes by @goto_scope 140 | # @param node [Node] node to match 141 | # @return [Array] matching nodes 142 | def find_nodes_by_goto_scope(node) 143 | @goto_scope.split('.').each { |scope| node = node.send(scope) } 144 | @rest.query_nodes(node) 145 | end 146 | 147 | # Find nodes by @relationship 148 | # @param node [Node] node to match 149 | # @return [Array] matching nodes 150 | def find_nodes_by_relationship(node) 151 | nodes = [] 152 | case @relationship 153 | when '>' 154 | if node.is_a?(::Array) 155 | node.each do |child_node| 156 | nodes << child_node if @rest.match?(child_node, child_node) 157 | end 158 | else 159 | @adapter.get_children(node).each do |child_node| 160 | if child_node.is_a?(Array) # SyntaxTree may return an array in child node. 161 | child_node.each do |child_child_node| 162 | nodes << child_child_node if @rest.match?(child_child_node, child_child_node) 163 | end 164 | elsif @adapter.is_node?(child_node) && :begin == @adapter.get_node_type(child_node) 165 | @adapter.get_children(child_node).each do |child_child_node| 166 | nodes << child_child_node if @rest.match?(child_child_node, child_child_node) 167 | end 168 | elsif @rest.match?(child_node, child_node) 169 | nodes << child_node 170 | end 171 | end 172 | end 173 | when '+' 174 | next_sibling = @adapter.get_siblings(node).first 175 | nodes << next_sibling if @rest.match?(next_sibling, next_sibling) 176 | when '~' 177 | @adapter.get_siblings(node).each do |sibling_node| 178 | nodes << sibling_node if @rest.match?(sibling_node, sibling_node) 179 | end 180 | end 181 | @rest.filter_by_position(nodes) 182 | end 183 | 184 | # Check if it matches pseudo class. 185 | # @param node [Node] node to match 186 | # @return [Boolean] 187 | def match_pseudo_class?(node) 188 | case @pseudo_class 189 | when 'has' 190 | !@pseudo_selector.query_nodes(node).empty? 191 | when 'not_has' 192 | @pseudo_selector.query_nodes(node).empty? 193 | else 194 | true 195 | end 196 | end 197 | end 198 | end 199 | -------------------------------------------------------------------------------- /lib/node_query/compiler/string.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module NodeQuery::Compiler 4 | # String represents a ruby string value. 5 | class String 6 | include Comparable 7 | 8 | # Initialize a String. 9 | # @param value [String] the string value 10 | # @param adapter [NodeQuery::Adapter] 11 | def initialize(value:, adapter:) 12 | @value = value 13 | @adapter = adapter 14 | end 15 | 16 | # Get the expected value. 17 | # @example 18 | # if the source code of the node is @id = id, 19 | # and the @value is "@{{right_vaue}}", 20 | # then it returns "@id". 21 | # @param base_node [Node] the base node for evaluated value 22 | # @return [String] the expected string, if it contains evaluated value, evaluate the node value. 23 | def expected_value(base_node) 24 | NodeQuery::Helper.evaluate_node_value(base_node, @value, @adapter) 25 | end 26 | 27 | # Check if the actual value equals the node value. 28 | # @param node [Node] the node 29 | # @param base_node [Node] the base node for evaluated value 30 | # @return [Boolean] true if the actual value equals the node value. 31 | def is_equal?(actual, expected) 32 | NodeQuery::Helper.to_string(actual, @adapter) == expected 33 | end 34 | 35 | # Get valid operators. 36 | # @return [Array] valid operators 37 | def valid_operators 38 | STRING_VALID_OPERATORS 39 | end 40 | 41 | def to_s 42 | "\"#{@value}\"" 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/node_query/compiler/symbol.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module NodeQuery::Compiler 4 | # Symbol represents a ruby symbol value. 5 | class Symbol 6 | include Comparable 7 | 8 | # Initliaze a Symobol. 9 | # @param value [Symbol] the symbol value 10 | # @param adapter [NodeQuery::Adapter] 11 | def initialize(value:, adapter:) 12 | @value = value 13 | @adapter = adapter 14 | end 15 | 16 | # Get valid operators. 17 | # @return [Array] valid operators 18 | def valid_operators 19 | SIMPLE_VALID_OPERATORS 20 | end 21 | 22 | def to_s 23 | ":#{@value}" 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/node_query/helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class NodeQuery::Helper 4 | class << self 5 | # Get target node by the keys. 6 | # @param node [Node] ast node 7 | # @param keys [String|Array] keys of child node. 8 | # @param adapter [NodeQuery::Adapter] 9 | # @return [Node|] the target node. 10 | def get_target_node(node, keys, adapter) 11 | return unless node 12 | 13 | first_key, rest_keys = keys.to_s.split('.', 2) 14 | if node.is_a?(Array) && first_key === "*" 15 | return node.map { |child_node| get_target_node(child_node, rest_keys, adapter) } 16 | end 17 | 18 | if node.is_a?(Array) && first_key =~ /\d+/ 19 | child_node = node[first_key.to_i] 20 | elsif node.respond_to?(first_key) 21 | child_node = node.send(first_key) 22 | elsif first_key == "node_type" 23 | child_node = adapter.get_node_type(node) 24 | end 25 | 26 | return child_node unless rest_keys 27 | 28 | return get_target_node(child_node, rest_keys, adapter) 29 | end 30 | 31 | # Recursively handle child nodes. 32 | # @param node [Node] ast node 33 | # @param adapter [NodeQuery::Adapter] adapter 34 | # @yield [child] Gives a child node. 35 | # @yieldparam child [Node] child node 36 | def handle_recursive_child(node, adapter, &block) 37 | adapter.get_children(node).each do |child| 38 | handle_child(child, adapter, &block) 39 | end 40 | end 41 | 42 | # Evaluate node value. 43 | # @example 44 | # source code of the node is @id = id 45 | # evaluated_node_value(node, "@{{value}}") # => @id 46 | # @param node [Node] ast node 47 | # @param str [String] string to be evaluated 48 | # @param adapter [NodeQuery::Adapter] adapter 49 | # @return [String] evaluated string 50 | def evaluate_node_value(node, str, adapter) 51 | str.scan(/{{(.+?)}}/).each do |match_data| 52 | target_node = NodeQuery::Helper.get_target_node(node, match_data.first, adapter) 53 | str = str.sub("{{#{match_data.first}}}", to_string(target_node, adapter)) 54 | end 55 | str 56 | end 57 | 58 | def to_string(node, adapter) 59 | if adapter.is_node?(node) 60 | return adapter.get_source(node) 61 | end 62 | 63 | node.to_s 64 | end 65 | 66 | private 67 | 68 | def handle_child(node, adapter, &block) 69 | if adapter.is_node?(node) 70 | block.call(node) 71 | handle_recursive_child(node, adapter, &block) 72 | elsif node.is_a?(Array) 73 | node.each do |child_node| 74 | handle_child(child_node, adapter, &block) unless child_node.nil? 75 | end 76 | end 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/node_query/node_rules.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class NodeQuery::NodeRules 4 | KEYWORDS = %i[not_includes includes not in not_in gt gte lt lte] 5 | 6 | # Initialize a NodeRules. 7 | # @param rules [Hash] the nod rules 8 | # @param adapter [NodeQuery::Adapter] 9 | def initialize(rules, adapter:) 10 | @rules = rules 11 | @adapter = adapter 12 | end 13 | 14 | # Query nodes by the rules. 15 | # @param node [Node] node to query 16 | # @param options [Hash] if query the current node 17 | # @option options [boolean] :including_self if query the current node, default is ture 18 | # @option options [boolean] :stop_at_first_match if stop at first match, default is false 19 | # @option options [boolean] :recursive if recursively query child nodes, default is true 20 | # @return [Array] matching nodes. 21 | def query_nodes(node, options = {}) 22 | options = { including_self: true, stop_at_first_match: false, recursive: true }.merge(options) 23 | if options[:including_self] && !options[:recursive] 24 | return match_node?(node) ? [node] : [] 25 | end 26 | 27 | matching_nodes = [] 28 | if options[:including_self] && match_node?(node) 29 | matching_nodes.push(node) 30 | return matching_nodes if options[:stop_at_first_match] 31 | end 32 | if options[:recursive] 33 | NodeQuery::Helper.handle_recursive_child(node, @adapter) do |child_node| 34 | if match_node?(child_node) 35 | matching_nodes.push(child_node) 36 | break if options[:stop_at_first_match] 37 | end 38 | end 39 | else 40 | @adapter.get_children(node).each do |child_node| 41 | if match_node?(child_node) 42 | matching_nodes.push(child_node) 43 | break if options[:stop_at_first_match] 44 | end 45 | end 46 | end 47 | matching_nodes 48 | end 49 | 50 | # Check if the node matches the rules. 51 | # @param node [Node] the node 52 | # @return [Boolean] 53 | def match_node?(node) 54 | flat_hash(@rules).keys.all? do |multi_keys| 55 | last_key = multi_keys.last 56 | actual = 57 | KEYWORDS.include?(last_key) ? 58 | NodeQuery::Helper.get_target_node(node, multi_keys[0...-1].join('.'), @adapter) : 59 | NodeQuery::Helper.get_target_node(node, multi_keys.join('.'), @adapter) 60 | expected = expected_value(@rules, multi_keys) 61 | expected = NodeQuery::Helper.evaluate_node_value(node, expected, @adapter) if expected.is_a?(String) 62 | case last_key 63 | when :includes 64 | actual.any? { |actual_value| match_value?(actual_value, expected) } 65 | when :not_includes 66 | actual.all? { |actual_value| !match_value?(actual_value, expected) } 67 | when :not 68 | !match_value?(actual, expected) 69 | when :in 70 | expected.any? { |expected_value| match_value?(actual, expected_value) } 71 | when :not_in 72 | expected.all? { |expected_value| !match_value?(actual, expected_value) } 73 | when :gt 74 | actual > expected 75 | when :gte 76 | actual >= expected 77 | when :lt 78 | actual < expected 79 | when :lte 80 | actual <= expected 81 | else 82 | match_value?(actual, expected) 83 | end 84 | end 85 | end 86 | 87 | private 88 | 89 | # Compare actual value with expected value. 90 | # 91 | # @param actual [Object] actual value. 92 | # @param expected [Object] expected value. 93 | # @return [Boolean] 94 | # @raise [NodeQuery::MethodNotSupported] if expected class is not supported. 95 | def match_value?(actual, expected) 96 | return true if actual == expected 97 | 98 | case expected 99 | when Symbol 100 | if @adapter.is_node?(actual) 101 | actual_source = @adapter.get_source(actual) 102 | actual_source == ":#{expected}" || actual_source == expected.to_s 103 | else 104 | actual&.to_sym == expected 105 | end 106 | when String 107 | if @adapter.is_node?(actual) 108 | actual_source = @adapter.get_source(actual) 109 | actual_source == expected || actual_source == unwrap_quote(expected) || 110 | unwrap_quote(actual_source) == expected || unwrap_quote(actual_source) == unwrap_quote(expected) 111 | else 112 | actual.to_s == expected || wrap_quote(actual.to_s) == expected 113 | end 114 | when Regexp 115 | if @adapter.is_node?(actual) 116 | actual.to_source =~ Regexp.new(expected.to_s, Regexp::MULTILINE) 117 | else 118 | actual.to_s =~ Regexp.new(expected.to_s, Regexp::MULTILINE) 119 | end 120 | when Array 121 | return false unless expected.length == actual.length 122 | 123 | actual.zip(expected).all? { |a, e| match_value?(a, e) } 124 | when NilClass 125 | if @adapter.is_node?(actual) 126 | actual.to_value.nil? 127 | else 128 | actual.nil? 129 | end 130 | when Numeric 131 | if @adapter.is_node?(actual) 132 | actual.children[0] == expected 133 | else 134 | actual == expected 135 | end 136 | when TrueClass 137 | true == actual&.to_value 138 | when FalseClass 139 | false == actual&.to_value 140 | else 141 | if @adapter.is_node?(expected) 142 | actual == expected 143 | else 144 | raise NodeQuery::MethodNotSupported, "#{expected} is not supported" 145 | end 146 | end 147 | end 148 | 149 | # Convert a hash to flat one. 150 | # 151 | # @example 152 | # flat_hash(node_type: 'block', caller: { node_type: 'send', receiver: 'RSpec' }) 153 | # # {[:node_type] => 'block', [:caller, :node_type] => 'send', [:caller, :receiver] => 'RSpec'} 154 | # @param h [Hash] original hash. 155 | # @return flatten hash. 156 | def flat_hash(h, k = []) 157 | new_hash = {} 158 | h.each_pair do |key, val| 159 | if val.is_a?(Hash) 160 | new_hash.merge!(flat_hash(val, k + [key])) 161 | else 162 | new_hash[k + [key]] = val 163 | end 164 | end 165 | new_hash 166 | end 167 | 168 | # Get expected value from rules. 169 | # 170 | # @param rules [Hash] 171 | # @param multi_keys [Array] 172 | # @return [Object] expected value. 173 | def expected_value(rules, multi_keys) 174 | multi_keys.inject(rules) { |o, key| o[key] } 175 | end 176 | 177 | # Wrap the string with single or double quote. 178 | # @param string [String] 179 | # @return [String] 180 | def wrap_quote(string) 181 | if string.include?("'") 182 | "\"#{string}\"" 183 | else 184 | "'#{string}'" 185 | end 186 | end 187 | 188 | # Unwrap the quote from the string. 189 | # @param string [String] 190 | # @return [String] 191 | def unwrap_quote(string) 192 | if (string[0] == '"' && string[-1] == '"') || (string[0] == "'" && string[-1] == "'") 193 | string[1...-1] 194 | else 195 | string 196 | end 197 | end 198 | end 199 | -------------------------------------------------------------------------------- /lib/node_query/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class NodeQuery 4 | VERSION = "1.16.0" 5 | end 6 | -------------------------------------------------------------------------------- /lib/node_query_lexer.rex: -------------------------------------------------------------------------------- 1 | class NodeQueryLexer 2 | 3 | macros 4 | OPEN_ATTRIBUTE /\[/ 5 | CLOSE_ATTRIBUTE /\]/ 6 | OPEN_ARRAY /\(/ 7 | CLOSE_ARRAY /\)/ 8 | OPEN_SELECTOR /\(/ 9 | CLOSE_SELECTOR /\)/ 10 | NODE_TYPE /\.[a-zA-Z_]+/ 11 | IDENTIFIER /[@\*\-\.\w]*\w/ 12 | IDENTIFIER_VALUE /[@\.\w!&:\?<>=]+/ 13 | OPERATOR /(\^=|\$=|\*=|!=|=~|!~|>=|<=|>|<|=|not includes|includes|not in|in)/i 14 | FALSE /false/ 15 | FLOAT /\-?\d+\.\d+/ 16 | INTEGER /\-?\d+/ 17 | NIL /nil/ 18 | REGEXP_BODY /(?:[^\/]|\\\/)*/ 19 | REGEXP /\/(#{REGEXP_BODY})(?=]+/ 21 | TRUE /true/ 22 | SINGLE_QUOTE_STRING /'.*?'/ 23 | DOUBLE_QUOTE_STRING /".*?"/ 24 | 25 | rules 26 | 27 | # [:state] pattern [actions] 28 | /\s+/ 29 | /,/ { [:tCOMMA, text] } 30 | /:first-child/ { [:tPOSITION, text[1..-1]] } 31 | /:last-child/ { [:tPOSITION, text[1..-1]] } 32 | /:has/ { [:tPSEUDO_CLASS, text[1..-1]] } 33 | /:not_has/ { [:tPSEUDO_CLASS, text[1..-1]] } 34 | /#{NODE_TYPE}/ { [:tNODE_TYPE, text[1..]] } 35 | /#{IDENTIFIER}/ { [:tGOTO_SCOPE, text] } 36 | />/ { [:tRELATIONSHIP, text] } 37 | /~/ { [:tRELATIONSHIP, text] } 38 | /\+/ { [:tRELATIONSHIP, text] } 39 | /#{OPEN_SELECTOR}/ { [:tOPEN_SELECTOR, text] } 40 | /#{CLOSE_SELECTOR}/ { [:tCLOSE_SELECTOR, text] } 41 | /#{OPEN_ATTRIBUTE}/ { @nested_count += 1; @state = :KEY; [:tOPEN_ATTRIBUTE, text] } 42 | :KEY /\s+/ 43 | :KEY /#{OPERATOR}/ { @state = :VALUE; [:tOPERATOR, text.downcase.sub(' ', '_')] } 44 | :KEY /#{IDENTIFIER}/ { [:tKEY, text] } 45 | :VALUE /\s+/ 46 | :VALUE /\[\]=/ { [:tIDENTIFIER_VALUE, text] } 47 | :VALUE /\[\]/ { [:tIDENTIFIER_VALUE, text] } 48 | :VALUE /:\[\]=/ { [:tSYMBOL, text[1..-1].to_sym] } 49 | :VALUE /:\[\]/ { [:tSYMBOL, text[1..-1].to_sym] } 50 | :VALUE /#{OPEN_ARRAY}/ { @state = :ARRAY_VALUE; [:tOPEN_ARRAY, text] } 51 | :VALUE /#{CLOSE_ATTRIBUTE}/ { @nested_count -= 1; @state = @nested_count == 0 ? nil : :VALUE; [:tCLOSE_ATTRIBUTE, text] } 52 | :VALUE /#{NIL}\?/ { [:tIDENTIFIER_VALUE, text] } 53 | :VALUE /#{NIL}/ { [:tNIL, nil] } 54 | :VALUE /#{TRUE}/ { [:tBOOLEAN, true] } 55 | :VALUE /#{FALSE}/ { [:tBOOLEAN, false] } 56 | :VALUE /#{SYMBOL}/ { [:tSYMBOL, text[1..-1].to_sym] } 57 | :VALUE /#{FLOAT}/ { [:tFLOAT, text.to_f] } 58 | :VALUE /#{INTEGER}/ { [:tINTEGER, text.to_i] } 59 | :VALUE /#{REGEXP}/ { [:tREGEXP, eval(text)] } 60 | :VALUE /#{DOUBLE_QUOTE_STRING}/ { [:tSTRING, text[1...-1]] } 61 | :VALUE /#{SINGLE_QUOTE_STRING}/ { [:tSTRING, text[1...-1]] } 62 | :VALUE /#{NODE_TYPE}/ { [:tNODE_TYPE, text[1..]] } 63 | :VALUE /#{OPEN_ATTRIBUTE}/ { @nested_count += 1; @state = :KEY; [:tOPEN_ATTRIBUTE, text] } 64 | :VALUE /#{IDENTIFIER_VALUE}/ { [:tIDENTIFIER_VALUE, text] } 65 | :ARRAY_VALUE /\s+/ 66 | :ARRAY_VALUE /#{CLOSE_ARRAY}/ { @state = :VALUE; [:tCLOSE_ARRAY, text] } 67 | :ARRAY_VALUE /#{NIL}\?/ { [:tIDENTIFIER_VALUE, text] } 68 | :ARRAY_VALUE /#{NIL}/ { [:tNIL, nil] } 69 | :ARRAY_VALUE /#{TRUE}/ { [:tBOOLEAN, true] } 70 | :ARRAY_VALUE /#{FALSE}/ { [:tBOOLEAN, false] } 71 | :ARRAY_VALUE /#{SYMBOL}/ { [:tSYMBOL, text[1..-1].to_sym] } 72 | :ARRAY_VALUE /#{FLOAT}/ { [:tFLOAT, text.to_f] } 73 | :ARRAY_VALUE /#{INTEGER}/ { [:tINTEGER, text.to_i] } 74 | :ARRAY_VALUE /#{REGEXP}/ { [:tREGEXP, eval(text)] } 75 | :ARRAY_VALUE /#{DOUBLE_QUOTE_STRING}/ { [:tSTRING, text[1...-1]] } 76 | :ARRAY_VALUE /#{SINGLE_QUOTE_STRING}/ { [:tSTRING, text[1...-1]] } 77 | :ARRAY_VALUE /#{IDENTIFIER_VALUE}/ { [:tIDENTIFIER_VALUE, text] } 78 | 79 | inner 80 | def initialize 81 | @nested_count = 0 82 | end 83 | 84 | def do_parse; end 85 | end -------------------------------------------------------------------------------- /lib/node_query_parser.y: -------------------------------------------------------------------------------- 1 | class NodeQueryParser 2 | options no_result_var 3 | token tCOMMA tNODE_TYPE tGOTO_SCOPE tKEY tIDENTIFIER_VALUE tPSEUDO_CLASS tRELATIONSHIP 4 | tOPEN_ATTRIBUTE tCLOSE_ATTRIBUTE tOPEN_ARRAY tCLOSE_ARRAY tOPEN_SELECTOR tCLOSE_SELECTOR tPOSITION 5 | tOPERATOR tARRAY_VALUE tBOOLEAN tFLOAT tINTEGER tNIL tREGEXP tSTRING tSYMBOL 6 | rule 7 | expression_list 8 | : expression tCOMMA expression_list { NodeQuery::Compiler::ExpressionList.new(expression: val[0], rest: val[2]) } 9 | | expression { NodeQuery::Compiler::ExpressionList.new(expression: val[0]) } 10 | 11 | expression 12 | : selector expression { NodeQuery::Compiler::Expression.new(selector: val[0], rest: val[1]) } 13 | | selector { NodeQuery::Compiler::Expression.new(selector: val[0]) } 14 | 15 | selector 16 | : basic_selector tPOSITION { NodeQuery::Compiler::Selector.new(basic_selector: val[0], position: val[1], adapter: @adapter ) } 17 | | basic_selector { NodeQuery::Compiler::Selector.new(basic_selector: val[0], adapter: @adapter) } 18 | | tPSEUDO_CLASS tOPEN_SELECTOR selector tCLOSE_SELECTOR { NodeQuery::Compiler::Selector.new(pseudo_class: val[0], pseudo_selector: val[2], adapter: @adapter) } 19 | | tRELATIONSHIP selector { NodeQuery::Compiler::Selector.new(relationship: val[0], rest: val[1], adapter: @adapter) } 20 | | tGOTO_SCOPE selector { NodeQuery::Compiler::Selector.new(goto_scope: val[0], rest: val[1], adapter: @adapter) } 21 | 22 | basic_selector 23 | : tNODE_TYPE { NodeQuery::Compiler::BasicSelector.new(node_type: val[0], adapter: @adapter) } 24 | | tNODE_TYPE attribute_list { NodeQuery::Compiler::BasicSelector.new(node_type: val[0], attribute_list: val[1], adapter: @adapter) } 25 | 26 | attribute_list 27 | : attribute attribute_list { NodeQuery::Compiler::AttributeList.new(attribute: val[0], rest: val[1]) } 28 | | attribute { NodeQuery::Compiler::AttributeList.new(attribute: val[0]) } 29 | 30 | attribute 31 | : tOPEN_ATTRIBUTE tKEY tOPERATOR value tCLOSE_ATTRIBUTE { NodeQuery::Compiler::Attribute.new(key: val[1], value: val[3], operator: val[2], adapter: @adapter) } 32 | | tOPEN_ATTRIBUTE tKEY tOPERATOR tOPEN_ARRAY tCLOSE_ARRAY tCLOSE_ATTRIBUTE { NodeQuery::Compiler::Attribute.new(key: val[1], value: NodeQuery::Compiler::ArrayValue.new(adapter: @adapter), operator: val[2], adapter: @adapter) } 33 | | tOPEN_ATTRIBUTE tKEY tOPERATOR tOPEN_ARRAY array_value tCLOSE_ARRAY tCLOSE_ATTRIBUTE { NodeQuery::Compiler::Attribute.new(key: val[1], value: val[4], operator: val[2], adapter: @adapter) } 34 | 35 | array_value 36 | : value array_value { NodeQuery::Compiler::ArrayValue.new(value: val[0], rest: val[1], adapter: @adapter) } 37 | | value { NodeQuery::Compiler::ArrayValue.new(value: val[0], adapter: @adapter) } 38 | 39 | value 40 | : selector 41 | | tBOOLEAN { NodeQuery::Compiler::Boolean.new(value: val[0], adapter: @adapter) } 42 | | tFLOAT { NodeQuery::Compiler::Float.new(value: val[0], adapter: @adapter) } 43 | | tINTEGER { NodeQuery::Compiler::Integer.new(value: val[0], adapter: @adapter) } 44 | | tNIL { NodeQuery::Compiler::Nil.new(value: val[0], adapter: @adapter) } 45 | | tREGEXP { NodeQuery::Compiler::Regexp.new(value: val[0], adapter: @adapter) } 46 | | tSTRING { NodeQuery::Compiler::String.new(value: val[0], adapter: @adapter) } 47 | | tSYMBOL { NodeQuery::Compiler::Symbol.new(value: val[0], adapter: @adapter) } 48 | | tIDENTIFIER_VALUE { NodeQuery::Compiler::Identifier.new(value: val[0], adapter: @adapter) } 49 | end 50 | 51 | ---- inner 52 | def initialize(adapter:) 53 | @lexer = NodeQueryLexer.new 54 | @adapter = adapter 55 | end 56 | 57 | def parse string 58 | @lexer.parse string 59 | do_parse 60 | end 61 | 62 | def next_token 63 | @lexer.next_token 64 | end 65 | -------------------------------------------------------------------------------- /node_query.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/node_query/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "node_query" 7 | spec.version = NodeQuery::VERSION 8 | spec.authors = ["Richard Huang"] 9 | spec.email = ["flyerhzm@gmail.com"] 10 | 11 | spec.summary = "ast node query language" 12 | spec.description = "ast node query language" 13 | spec.homepage = "https://github.com/synvert-hq/node-query-ruby" 14 | spec.required_ruby_version = ">= 2.7.0" 15 | 16 | spec.metadata["homepage_uri"] = spec.homepage 17 | spec.metadata["source_code_uri"] = "https://github.com/synvert-hq/node-query-ruby" 18 | spec.metadata["changelog_uri"] = "https://github.com/synvert-hq/node-query-ruby/blob/master/CHANGELOG.md" 19 | 20 | # Specify which files should be added to the gem when it is released. 21 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 22 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 23 | `git ls-files -z`.split("\x0").reject do |f| 24 | (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)}) 25 | end 26 | end + %w[lib/node_query_lexer.rex.rb lib/node_query_parser.racc.rb] 27 | spec.bindir = "exe" 28 | spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } 29 | spec.require_paths = ["lib"] 30 | 31 | # For more information and examples about making a new gem, check out our 32 | # guide at: https://bundler.io/guides/creating_gem.html 33 | end 34 | -------------------------------------------------------------------------------- /sig/node_query.rbs: -------------------------------------------------------------------------------- 1 | class NodeQuery[T] 2 | VERSION: String 3 | 4 | def initialize: (nqlOrRues: String | Hash, adapter: Symbol) -> NodeQuery 5 | 6 | def query_nodes: (node: T, options: Hash) -> Array[T] 7 | 8 | def match_node?: (node: T) -> boolean 9 | end 10 | -------------------------------------------------------------------------------- /sig/node_query/adapter.rbs: -------------------------------------------------------------------------------- 1 | class NodeQuery::Adapter[T] 2 | def is_node?: (node: T) -> bool 3 | 4 | def get_node_type: (node: T) -> String 5 | 6 | def get_source: (node: T) -> String 7 | 8 | def get_children: (node: T) -> Array[T] 9 | 10 | def get_siblings: (node: T) -> Array[T] 11 | end 12 | -------------------------------------------------------------------------------- /spec/node_query/adapter/parser_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe NodeQuery::ParserAdapter do 6 | let(:adapter) { described_class.new } 7 | 8 | describe "#is_node?" do 9 | it 'gets true for node' do 10 | node = parser_parse("class Synvert; end") 11 | expect(adapter.is_node?(node)).to be_truthy 12 | end 13 | 14 | it 'gets false for other' do 15 | expect(adapter.is_node?("Synvert")).to be_falsey 16 | end 17 | end 18 | 19 | describe "#get_node_type" do 20 | it "gets the type of node" do 21 | node = parser_parse("class Synvert; end") 22 | expect(adapter.get_node_type(node)).to eq :class 23 | end 24 | end 25 | 26 | describe "#get_source" do 27 | it "gets the source code of node" do 28 | code = "class Synvert; end" 29 | node = parser_parse(code) 30 | expect(adapter.get_source(node)).to eq code 31 | end 32 | end 33 | 34 | describe "#get_children" do 35 | it "gets the children of node" do 36 | node = parser_parse("class Synvert; end") 37 | expect(adapter.get_children(node)).to eq [parser_parse("Synvert"), nil, nil] 38 | end 39 | end 40 | 41 | describe "#get_siblings" do 42 | it "gets the siblings of node" do 43 | node = parser_parse("class Synvert; end").children.first 44 | expect(adapter.get_siblings(node)).to eq [nil, nil] 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/node_query/adapter/prism_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe NodeQuery::PrismAdapter do 6 | let(:adapter) { described_class.new } 7 | 8 | describe "#is_node?" do 9 | it 'gets true for node' do 10 | node = prism_parse("class Synvert; end").body.first 11 | expect(adapter.is_node?(node)).to be_truthy 12 | end 13 | 14 | it 'gets false for other' do 15 | expect(adapter.is_node?("Synvert")).to be_falsey 16 | end 17 | end 18 | 19 | describe "#get_node_type" do 20 | it "gets the type of node" do 21 | node = prism_parse("class Synvert; end").body.first 22 | expect(adapter.get_node_type(node)).to eq :class_node 23 | end 24 | end 25 | 26 | describe "#get_source" do 27 | it "gets the source code of node" do 28 | code = "class Synvert; end" 29 | node = prism_parse(code).body.first 30 | expect(adapter.get_source(node)).to eq code 31 | end 32 | end 33 | 34 | describe "#get_children" do 35 | it "gets the children of node" do 36 | node = prism_parse("class Synvert; end").body.first 37 | child_nodes = adapter.get_children(node) 38 | expect(child_nodes.size).to eq 8 39 | expect(child_nodes[1]).to eq 'class' 40 | expect(child_nodes[2].class).to eq Prism::ConstantReadNode 41 | expect(child_nodes[2].name).to eq :Synvert 42 | expect(child_nodes[7]).to eq :Synvert 43 | end 44 | end 45 | 46 | describe "#get_siblings" do 47 | it "gets the siblings of node" do 48 | node = prism_parse("class Synvert; end").body.first.constant_path 49 | siblings = adapter.get_siblings(node) 50 | expect(siblings.size).to eq 5 51 | expect(siblings[0]).to be_nil 52 | expect(siblings[1]).to be_nil 53 | expect(siblings[4]).to eq :Synvert 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/node_query/adapter/syntax_tree_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe NodeQuery::SyntaxTreeAdapter do 6 | let(:adapter) { described_class.new } 7 | 8 | describe "#is_node?" do 9 | it 'gets true for node' do 10 | node = syntax_tree_parse("class Synvert; end").body.first 11 | expect(adapter.is_node?(node)).to be_truthy 12 | end 13 | 14 | it 'gets false for other' do 15 | expect(adapter.is_node?("Synvert")).to be_falsey 16 | end 17 | end 18 | 19 | describe "#get_node_type" do 20 | it "gets the type of node" do 21 | node = syntax_tree_parse("class Synvert; end").body.first 22 | expect(adapter.get_node_type(node)).to eq :ClassDeclaration 23 | end 24 | end 25 | 26 | describe "#get_source" do 27 | it "gets the source code of node" do 28 | code = "class Synvert; end" 29 | node = syntax_tree_parse(code).body.first 30 | expect(adapter.get_source(node)).to eq code 31 | end 32 | end 33 | 34 | describe "#get_children" do 35 | it "gets the children of node" do 36 | node = syntax_tree_parse("class Synvert; end").body.first 37 | child_nodes = adapter.get_children(node) 38 | expect(child_nodes.size).to eq 3 39 | expect(child_nodes[0].class).to eq SyntaxTree::ConstRef 40 | expect(child_nodes[1]).to be_nil 41 | expect(child_nodes[2].class).to eq SyntaxTree::BodyStmt 42 | end 43 | end 44 | 45 | describe "#get_siblings" do 46 | it "gets the siblings of node" do 47 | node = syntax_tree_parse("class Synvert; end").body.first.constant 48 | siblings = adapter.get_siblings(node) 49 | expect(siblings.size).to eq 2 50 | expect(siblings[0]).to be_nil 51 | expect(siblings[1].class).to eq SyntaxTree::BodyStmt 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/node_query/helper_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe NodeQuery::Helper do 6 | describe '.get_target_node' do 7 | it 'checks node_type' do 8 | node = parser_parse('Factory.define :user do |user|; end') 9 | child_node = described_class.get_target_node(node, 'node_type', NodeQuery::ParserAdapter.new) 10 | expect(child_node).to eq :block 11 | end 12 | 13 | context 'block node' do 14 | it 'checks caller' do 15 | node = parser_parse('Factory.define :user do |user|; end') 16 | child_node = described_class.get_target_node(node, 'caller', NodeQuery::ParserAdapter.new) 17 | expect(child_node).to eq node.caller 18 | end 19 | 20 | it 'checks arguments' do 21 | node = parser_parse('Factory.define :user do |user|; end') 22 | child_node = described_class.get_target_node(node, 'arguments', NodeQuery::ParserAdapter.new) 23 | expect(child_node).to eq node.arguments 24 | end 25 | 26 | it 'checks caller.receiver' do 27 | node = parser_parse('Factory.define :user do |user|; end') 28 | child_node = described_class.get_target_node(node, 'caller.receiver', NodeQuery::ParserAdapter.new) 29 | expect(child_node).to eq node.caller.receiver 30 | end 31 | 32 | it 'checks caller.message' do 33 | node = parser_parse('Factory.define :user do |user|; end') 34 | child_node = described_class.get_target_node(node, 'caller.message', NodeQuery::ParserAdapter.new) 35 | expect(child_node).to eq node.caller.message 36 | end 37 | end 38 | 39 | context 'array' do 40 | it 'checks array by index' do 41 | node = parser_parse('factory :admin, class: User do; end') 42 | child_node = described_class.get_target_node(node, 'caller.arguments.1', NodeQuery::ParserAdapter.new) 43 | expect(child_node).to eq node.caller.arguments[1] 44 | end 45 | 46 | it 'checks array by method' do 47 | node = parser_parse('factory :admin, class: User do; end') 48 | child_node = described_class.get_target_node(node, 'caller.arguments.first', NodeQuery::ParserAdapter.new) 49 | expect(child_node).to eq node.caller.arguments.first 50 | end 51 | end 52 | end 53 | 54 | describe '.handle_recursive_child' do 55 | it 'recursively handle all children' do 56 | node = parser_parse('class Synvert; def current_node; @node; end; end') 57 | children = [] 58 | described_class.handle_recursive_child(node, NodeQuery::ParserAdapter.new) { |child| children << child.type } 59 | expect(children).to be_include :const 60 | expect(children).to be_include :def 61 | expect(children).to be_include :args 62 | expect(children).to be_include :ivar 63 | end 64 | end 65 | 66 | describe '.evaluate_node_value' do 67 | it 'returns an evaluated string' do 68 | node = parser_parse('@id = id') 69 | value = described_class.evaluate_node_value(node, "@{{value}}", NodeQuery::ParserAdapter.new) 70 | expect(value).to eq '@id' 71 | end 72 | end 73 | 74 | describe '.to_string' do 75 | it 'gets source code of the node' do 76 | node = parser_parse('@id = id') 77 | expect(described_class.to_string(node, NodeQuery::ParserAdapter.new)).to eq '@id = id' 78 | end 79 | 80 | it 'gets string' do 81 | expect(described_class.to_string(true, NodeQuery::ParserAdapter.new)).to eq 'true' 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /spec/node_query/node_rules/parser_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe NodeQuery::NodeRules do 4 | describe '#query_nodes' do 5 | context 'parser' do 6 | let(:adapter) { NodeQuery::ParserAdapter.new } 7 | let(:node) { 8 | parser_parse(<<~EOS) 9 | class User < Base 10 | def initialize(id, name) 11 | @id = id 12 | @name = name 13 | end 14 | end 15 | 16 | user = User.new(1, "Murphy") 17 | EOS 18 | } 19 | 20 | it 'matches node type' do 21 | rules = described_class.new({ node_type: 'def' }, adapter: adapter) 22 | expect(rules.query_nodes(node)).to eq node.body.first.body 23 | end 24 | 25 | it 'matches node type and one attribute' do 26 | rules = described_class.new({ node_type: 'class', name: 'User' }, adapter: adapter) 27 | expect(rules.query_nodes(node)).to eq [node.body.first] 28 | end 29 | 30 | it 'matches multiple attributes' do 31 | rules = described_class.new( 32 | { node_type: 'def', arguments: { size: 2, '0': 'id', '1': 'name' } }, 33 | adapter: adapter 34 | ) 35 | expect(rules.query_nodes(node)).to eq node.body.first.body 36 | end 37 | 38 | it 'matches nested attribute' do 39 | rules = described_class.new({ node_type: 'class', parent_class: { name: 'Base' } }, adapter: adapter) 40 | expect(rules.query_nodes(node)).to eq [node.body.first] 41 | end 42 | 43 | it 'matches not' do 44 | rules = described_class.new({ node_type: 'def', name: { not: 'foobar' } }, adapter: adapter) 45 | expect(rules.query_nodes(node)).to eq node.body.first.body 46 | end 47 | 48 | it 'matches regex' do 49 | rules = described_class.new({ node_type: 'def', name: /init/ }, adapter: adapter) 50 | expect(rules.query_nodes(node)).to eq node.body.first.body 51 | end 52 | 53 | it 'matches regex not' do 54 | rules = described_class.new({ node_type: 'def', name: { not: /foobar/ } }, adapter: adapter) 55 | expect(rules.query_nodes(node)).to eq node.body.first.body 56 | end 57 | 58 | it 'matches in' do 59 | rules = described_class.new({ node_type: 'ivasgn', variable: { in: ['@id', '@name'] } }, adapter: adapter) 60 | expect(rules.query_nodes(node)).to eq node.body.first.body.first.body 61 | end 62 | 63 | it 'matches not_in' do 64 | rules = described_class.new({ node_type: 'ivasgn', variable: { not_in: ['@id', '@name'] } }, adapter: adapter) 65 | expect(rules.query_nodes(node)).to eq [] 66 | end 67 | 68 | it 'matches includes' do 69 | rules = described_class.new({ node_type: 'def', arguments: { includes: 'id' } }, adapter: adapter) 70 | expect(rules.query_nodes(node)).to eq node.body.first.body 71 | end 72 | 73 | it 'matches not_includes' do 74 | rules = described_class.new({ node_type: 'def', arguments: { not_includes: 'foobar' } }, adapter: adapter) 75 | expect(rules.query_nodes(node)).to eq node.body.first.body 76 | end 77 | 78 | it 'matches equal array' do 79 | rules = described_class.new({ node_type: 'def', arguments: ['id', 'name'] }, adapter: adapter) 80 | expect(rules.query_nodes(node)).to eq node.body.first.body 81 | 82 | rules = described_class.new({ node_type: 'def', arguments: ['name', 'id'] }, adapter: adapter) 83 | expect(rules.query_nodes(node)).to eq [] 84 | end 85 | 86 | it 'matches not equal array' do 87 | rules = described_class.new({ node_type: 'def', arguments: { not: ['id', 'name'] } }, adapter: adapter) 88 | expect(rules.query_nodes(node)).to eq [] 89 | 90 | rules = described_class.new({ node_type: 'def', arguments: { not: ['name', 'id'] } }, adapter: adapter) 91 | expect(rules.query_nodes(node)).to eq node.body.first.body 92 | end 93 | 94 | it 'matches nested selector' do 95 | rules = described_class.new({ node_type: 'def', body: { '0': { node_type: 'ivasgn' } } }, adapter: adapter) 96 | expect(rules.query_nodes(node)).to eq node.body.first.body 97 | end 98 | 99 | it 'matches gte' do 100 | rules = described_class.new({ node_type: 'def', arguments: { size: { gte: 2 } } }, adapter: adapter) 101 | expect(rules.query_nodes(node)).to eq node.body.first.body 102 | end 103 | 104 | it 'matches gt' do 105 | rules = described_class.new({ node_type: 'def', arguments: { size: { gt: 2 } } }, adapter: adapter) 106 | expect(rules.query_nodes(node)).to eq [] 107 | end 108 | 109 | it 'matches lte' do 110 | rules = described_class.new({ node_type: 'def', arguments: { size: { lte: 2 } } }, adapter: adapter) 111 | expect(rules.query_nodes(node)).to eq node.body.first.body 112 | end 113 | 114 | it 'matches lt' do 115 | rules = described_class.new({ node_type: 'def', arguments: { size: { lt: 2 } } }, adapter: adapter) 116 | expect(rules.query_nodes(node)).to eq [] 117 | end 118 | 119 | it 'matches arguments' do 120 | rules = described_class.new( 121 | { 122 | node_type: 'send', 123 | arguments: { size: 2, first: { node_type: 'int' }, last: { node_type: 'str' } } 124 | }, 125 | adapter: adapter 126 | ) 127 | expect(rules.query_nodes(node)).to eq [node.body.last.value] 128 | 129 | rules = described_class.new( 130 | { 131 | node_type: 'send', 132 | arguments: { size: 2, '0': { node_type: 'int' }, '-1': { node_type: 'str' } } 133 | }, 134 | adapter: adapter 135 | ) 136 | expect(rules.query_nodes(node)).to eq [node.body.last.value] 137 | end 138 | 139 | it 'matches evaluated value' do 140 | rules = described_class.new({ node_type: 'ivasgn', variable: '@{{value}}' }, adapter: adapter) 141 | expect(rules.query_nodes(node)).to eq node.body.first.body.first.body 142 | end 143 | 144 | it 'matches evaluated value from base node' do 145 | rules = described_class.new( 146 | { 147 | node_type: 'def', 148 | name: 'initialize', 149 | body: { '0': { variable: "@{{body.0.value}}" } } 150 | }, 151 | adapter: adapter 152 | ) 153 | expect(rules.query_nodes(node)).to eq node.body.first.body 154 | end 155 | 156 | it 'matches []' do 157 | node = parser_parse("user[:error]") 158 | rules = described_class.new({ node_type: 'send', message: :[] }, adapter: adapter) 159 | expect(rules.query_nodes(node)).to eq [node] 160 | end 161 | 162 | it 'matches []=' do 163 | node = parser_parse("user[:error] = 'error'") 164 | rules = described_class.new({ node_type: 'send', message: :[]= }, adapter: adapter) 165 | expect(rules.query_nodes(node)).to eq [node] 166 | end 167 | 168 | it 'matches nil and nil?' do 169 | node = parser_parse("nil.nil?") 170 | rules = described_class.new({ node_type: 'send', reciever: nil, message: :nil? }, adapter: adapter) 171 | expect(rules.query_nodes(node)).to eq [node] 172 | end 173 | 174 | it 'matches empty string' do 175 | node = parser_parse("call('')") 176 | rules = described_class.new({ node_type: 'send', message: :call, arguments: { first: '' } }, adapter: adapter) 177 | expect(rules.query_nodes(node)).to eq [node] 178 | end 179 | 180 | it 'matches hash value' do 181 | node = parser_parse("{ foo: 'bar' }") 182 | rules = described_class.new({ node_type: 'hash', foo_value: 'bar' }, adapter: adapter) 183 | expect(rules.query_nodes(node)).to eq [node] 184 | end 185 | 186 | it 'raises error' do 187 | node = parser_parse("Foobar.stub :new, &block") 188 | rules = described_class.new( 189 | { 190 | node_type: 'send', 191 | message: 'stub', 192 | arguments: [{ type: 'sym' }, { type: 'block_pass' }] 193 | }, 194 | adapter: adapter 195 | ) 196 | 197 | expected_error_message = 198 | if RUBY_VERSION >= '3.4.0' 199 | '{type: "sym"} is not supported' 200 | else 201 | '{:type=>"sym"} is not supported' 202 | end 203 | 204 | expect { 205 | rules.query_nodes(node) 206 | }.to raise_error(NodeQuery::MethodNotSupported, expected_error_message) 207 | end 208 | 209 | it 'sets option including_self to false' do 210 | rules = described_class.new({ node_type: 'class' }, adapter: adapter) 211 | expect(rules.query_nodes(node.children.first, { including_self: false })).to eq [] 212 | 213 | expect(rules.query_nodes(node.children.first)).to eq [node.children.first] 214 | end 215 | 216 | it 'sets options stop_at_first_match to true' do 217 | rules = described_class.new({ node_type: 'ivasgn' }, adapter: adapter) 218 | expect( 219 | rules.query_nodes( 220 | node.children.first, 221 | { stop_at_first_match: true } 222 | ) 223 | ).to eq [node.children.first.body.first.body.first] 224 | 225 | # expect(rules.query_nodes(node.children.first)).to eq node.children.first.body.first.body 226 | end 227 | 228 | it 'sets options recursive to false' do 229 | rules = described_class.new({ node_type: 'def' }, adapter: adapter) 230 | expect(rules.query_nodes(node, { recursive: false })).to eq [] 231 | 232 | expect(rules.query_nodes(node)).to eq [node.children.first.body.first] 233 | end 234 | end 235 | end 236 | end 237 | -------------------------------------------------------------------------------- /spec/node_query/node_rules/prism_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe NodeQuery::NodeRules do 4 | describe '#query_nodes' do 5 | context 'prism' do 6 | let(:adapter) { NodeQuery::PrismAdapter.new } 7 | let(:node) { 8 | prism_parse(<<~EOS) 9 | class User < Base 10 | def initialize(id, name) 11 | @id = id 12 | @name = name 13 | end 14 | end 15 | 16 | user = User.new(1, "Murphy") 17 | EOS 18 | } 19 | 20 | it 'matches node type', focus: true do 21 | rules = described_class.new({ node_type: 'def_node' }, adapter: adapter) 22 | expect(rules.query_nodes(node)).to eq [node.body.first.body.body.first] 23 | end 24 | 25 | it 'matches node type and one attribute' do 26 | rules = described_class.new({ node_type: 'class_node', constant_path: 'User' }, adapter: adapter) 27 | expect(rules.query_nodes(node)).to eq [node.body.first] 28 | end 29 | 30 | it 'matches multiple attributes' do 31 | rules = described_class.new( 32 | { 33 | node_type: 'def_node', 34 | parameters: { requireds: { size: 2, '0': 'id', '1': 'name' } } 35 | }, 36 | adapter: adapter 37 | ) 38 | expect(rules.query_nodes(node)).to eq [node.body.first.body.body.first] 39 | end 40 | 41 | it 'matches nested attribute' do 42 | rules = described_class.new({ node_type: 'class_node', superclass: { name: 'Base' } }, adapter: adapter) 43 | expect(rules.query_nodes(node)).to eq [node.body.first] 44 | end 45 | 46 | it 'matches not' do 47 | rules = described_class.new({ node_type: 'def_node', name: { not: 'foobar' } }, adapter: adapter) 48 | expect(rules.query_nodes(node)).to eq [node.body.first.body.body.first] 49 | end 50 | 51 | it 'matches regex' do 52 | rules = described_class.new({ node_type: 'def_node', name: /init/ }, adapter: adapter) 53 | expect(rules.query_nodes(node)).to eq [node.body.first.body.body.first] 54 | end 55 | 56 | it 'matches regex not' do 57 | rules = described_class.new({ node_type: 'def_node', name: { not: /foobar/ } }, adapter: adapter) 58 | expect(rules.query_nodes(node)).to eq [node.body.first.body.body.first] 59 | end 60 | 61 | it 'matches in' do 62 | rules = described_class.new( 63 | { node_type: 'instance_variable_write_node', name: { in: ['@id', '@name'] } }, 64 | adapter: adapter 65 | ) 66 | expect(rules.query_nodes(node)).to eq [ 67 | node.body.first.body.body.first.body.body.first, 68 | node.body.first.body.body.first.body.body.last 69 | ] 70 | end 71 | 72 | it 'matches not_in' do 73 | rules = described_class.new( 74 | { node_type: 'instance_variable_write_node', name: { not_in: ['@id', '@name'] } }, 75 | adapter: adapter 76 | ) 77 | expect(rules.query_nodes(node)).to eq [] 78 | end 79 | 80 | it 'matches includes' do 81 | rules = described_class.new( 82 | { node_type: 'def_node', parameters: { requireds: { includes: 'id' } } }, 83 | adapter: adapter 84 | ) 85 | expect(rules.query_nodes(node)).to eq [node.body.first.body.body.first] 86 | end 87 | 88 | it 'matches not_includes' do 89 | rules = described_class.new( 90 | { 91 | node_type: 'def_node', 92 | parameters: { requireds: { not_includes: 'foobar' } } 93 | }, 94 | adapter: adapter 95 | ) 96 | expect(rules.query_nodes(node)).to eq [node.body.first.body.body.first] 97 | end 98 | 99 | it 'matches equal array' do 100 | rules = described_class.new( 101 | { node_type: 'def_node', parameters: { requireds: ['id', 'name'] } }, 102 | adapter: adapter 103 | ) 104 | expect(rules.query_nodes(node)).to eq [node.body.first.body.body.first] 105 | 106 | rules = described_class.new( 107 | { node_type: 'def_node', parameters: { requireds: ['name', 'id'] } }, 108 | adapter: adapter 109 | ) 110 | expect(rules.query_nodes(node)).to eq [] 111 | end 112 | 113 | it 'matches not equal array' do 114 | rules = described_class.new( 115 | { 116 | node_type: 'def_node', 117 | parameters: { requireds: { not: ['id', 'name'] } } 118 | }, 119 | adapter: adapter 120 | ) 121 | expect(rules.query_nodes(node)).to eq [] 122 | 123 | rules = described_class.new( 124 | { 125 | node_type: 'def_node', 126 | parameters: { requireds: { not: ['name', 'id'] } } 127 | }, 128 | adapter: adapter 129 | ) 130 | expect(rules.query_nodes(node)).to eq [node.body.first.body.body.first] 131 | end 132 | 133 | it 'matches nested selector' do 134 | rules = described_class.new( 135 | { 136 | node_type: 'def_node', 137 | body: { body: { '0': { node_type: 'instance_variable_write_node' } } } 138 | }, 139 | adapter: adapter 140 | ) 141 | expect(rules.query_nodes(node)).to eq [node.body.first.body.body.first] 142 | end 143 | 144 | it 'matches gte' do 145 | rules = described_class.new( 146 | { node_type: 'def_node', parameters: { requireds: { size: { gte: 2 } } } }, adapter: adapter 147 | ) 148 | expect(rules.query_nodes(node)).to eq [node.body.first.body.body.first] 149 | end 150 | 151 | it 'matches gt' do 152 | rules = described_class.new( 153 | { node_type: 'def_node', parameters: { requireds: { size: { ge: 2 } } } }, 154 | adapter: adapter 155 | ) 156 | expect(rules.query_nodes(node)).to eq [] 157 | end 158 | 159 | it 'matches lte' do 160 | rules = described_class.new( 161 | { node_type: 'def_node', parameters: { requireds: { size: { lte: 2 } } } }, adapter: adapter 162 | ) 163 | expect(rules.query_nodes(node)).to eq [node.body.first.body.body.first] 164 | end 165 | 166 | it 'matches lt' do 167 | rules = described_class.new( 168 | { node_type: 'def_node', parameters: { requireds: { size: { lt: 2 } } } }, 169 | adapter: adapter 170 | ) 171 | expect(rules.query_nodes(node)).to eq [] 172 | end 173 | 174 | it 'matches arguments' do 175 | rules = described_class.new( 176 | { 177 | node_type: 'call_node', 178 | arguments: { 179 | arguments: { 180 | size: 2, 181 | first: { node_type: 'integer_node' }, 182 | last: { node_type: 'string_node' } 183 | } 184 | } 185 | }, 186 | adapter: adapter 187 | ) 188 | expect(rules.query_nodes(node)).to eq [node.body.last.value] 189 | 190 | rules = described_class.new( 191 | { 192 | node_type: 'call_node', 193 | arguments: { 194 | arguments: { 195 | size: 2, 196 | '0': { node_type: 'integer_node' }, 197 | '-1': { node_type: 'string_node' } 198 | } 199 | } 200 | }, 201 | adapter: adapter 202 | ) 203 | expect(rules.query_nodes(node)).to eq [node.body.last.value] 204 | end 205 | 206 | it 'matches evaluated value' do 207 | rules = described_class.new({ node_type: 'instance_variable_write_node', name: '@{{value}}' }, adapter: adapter) 208 | expect(rules.query_nodes(node)).to eq node.body.first.body.body.first.body.body 209 | end 210 | 211 | it 'matches evaluated value from base node' do 212 | rules = described_class.new( 213 | { 214 | node_type: 'def_node', 215 | name: 'initialize', 216 | body: { body: { '0': { name: "@{{body.body.0.value}}" } } } 217 | }, 218 | adapter: adapter 219 | ) 220 | expect(rules.query_nodes(node)).to eq [node.body.first.body.body.first] 221 | end 222 | 223 | it 'matches empty string' do 224 | node = prism_parse("call('')") 225 | rules = described_class.new( 226 | { 227 | node_type: 'call_node', 228 | message: :call, 229 | arguments: { arguments: { first: '' } } 230 | }, 231 | adapter: adapter 232 | ) 233 | expect(rules.query_nodes(node)).to eq [node.body.first] 234 | end 235 | 236 | it 'matches hash value' do 237 | node = prism_parse("{ foo: 'bar' }") 238 | rules = described_class.new({ node_type: 'hash_node', foo_value: 'bar' }, adapter: adapter) 239 | expect(rules.query_nodes(node)).to eq [node.body.first] 240 | end 241 | 242 | it 'matches missing hash value' do 243 | node = prism_parse("{ foo: 'bar' }") 244 | rules = described_class.new({ node_type: 'hash_node', bar_value: :foo }, adapter: adapter) 245 | expect(rules.query_nodes(node)).to eq [] 246 | end 247 | end 248 | end 249 | end 250 | -------------------------------------------------------------------------------- /spec/node_query/node_rules/syntax_tree_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe NodeQuery::NodeRules do 4 | describe '#query_nodes' do 5 | context 'syntax_tree' do 6 | let(:adapter) { NodeQuery::SyntaxTreeAdapter.new } 7 | let(:node) { 8 | syntax_tree_parse(<<~EOS) 9 | class User < Base 10 | def initialize(id, name) 11 | @id = id 12 | @name = name 13 | end 14 | end 15 | 16 | user = User.new(1, "Murphy") 17 | EOS 18 | } 19 | 20 | it 'matches node type' do 21 | rules = described_class.new({ node_type: 'DefNode' }, adapter: adapter) 22 | expect(rules.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 23 | end 24 | 25 | it 'matches node type and one attribute' do 26 | rules = described_class.new({ node_type: 'ClassDeclaration', constant: 'User' }, adapter: adapter) 27 | expect(rules.query_nodes(node)).to eq [node.body.first] 28 | end 29 | 30 | it 'matches multiple attributes' do 31 | rules = described_class.new( 32 | { 33 | node_type: 'DefNode', 34 | params: { contents: { requireds: { size: 2, '0': 'id', '1': 'name' } } } 35 | }, 36 | adapter: adapter 37 | ) 38 | expect(rules.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 39 | end 40 | 41 | it 'matches nested attribute' do 42 | rules = described_class.new({ node_type: 'ClassDeclaration', superclass: { value: 'Base' } }, adapter: adapter) 43 | expect(rules.query_nodes(node)).to eq [node.body.first] 44 | end 45 | 46 | it 'matches not' do 47 | rules = described_class.new({ node_type: 'DefNode', name: { not: 'foobar' } }, adapter: adapter) 48 | expect(rules.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 49 | end 50 | 51 | it 'matches regex' do 52 | rules = described_class.new({ node_type: 'DefNode', name: /init/ }, adapter: adapter) 53 | expect(rules.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 54 | end 55 | 56 | it 'matches regex not' do 57 | rules = described_class.new({ node_type: 'DefNode', name: { not: /foobar/ } }, adapter: adapter) 58 | expect(rules.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 59 | end 60 | 61 | it 'matches in' do 62 | rules = described_class.new({ node_type: 'IVar', value: { in: ['@id', '@name'] } }, adapter: adapter) 63 | expect(rules.query_nodes(node)).to eq [ 64 | node.body.first.bodystmt.statements.body.first.bodystmt.statements.body.first.target.value, 65 | node.body.first.bodystmt.statements.body.first.bodystmt.statements.body.last.target.value 66 | ] 67 | end 68 | 69 | it 'matches not_in' do 70 | rules = described_class.new({ node_type: 'IVar', value: { not_in: ['@id', '@name'] } }, adapter: adapter) 71 | expect(rules.query_nodes(node)).to eq [] 72 | end 73 | 74 | it 'matches includes' do 75 | rules = described_class.new( 76 | { node_type: 'DefNode', params: { contents: { requireds: { includes: 'id' } } } }, 77 | adapter: adapter 78 | ) 79 | expect(rules.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 80 | end 81 | 82 | it 'matches not_includes' do 83 | rules = described_class.new( 84 | { 85 | node_type: 'DefNode', 86 | params: { contents: { requireds: { not_includes: 'foobar' } } } 87 | }, 88 | adapter: adapter 89 | ) 90 | expect(rules.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 91 | end 92 | 93 | it 'matches equal array' do 94 | rules = described_class.new( 95 | { node_type: 'DefNode', params: { contents: { requireds: ['id', 'name'] } } }, 96 | adapter: adapter 97 | ) 98 | expect(rules.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 99 | 100 | rules = described_class.new( 101 | { node_type: 'DefNode', params: { contents: { requireds: ['name', 'id'] } } }, 102 | adapter: adapter 103 | ) 104 | expect(rules.query_nodes(node)).to eq [] 105 | end 106 | 107 | it 'matches not equal array' do 108 | rules = described_class.new( 109 | { 110 | node_type: 'DefNode', 111 | params: { contents: { requireds: { not: ['id', 'name'] } } } 112 | }, 113 | adapter: adapter 114 | ) 115 | expect(rules.query_nodes(node)).to eq [] 116 | 117 | rules = described_class.new( 118 | { 119 | node_type: 'DefNode', 120 | params: { contents: { requireds: { not: ['name', 'id'] } } } 121 | }, 122 | adapter: adapter 123 | ) 124 | expect(rules.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 125 | end 126 | 127 | it 'matches nested selector' do 128 | rules = described_class.new( 129 | { 130 | node_type: 'DefNode', 131 | bodystmt: { statements: { body: { '0': { node_type: 'Assign' } } } } 132 | }, 133 | adapter: adapter 134 | ) 135 | expect(rules.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 136 | end 137 | 138 | it 'matches gte' do 139 | rules = described_class.new( 140 | { node_type: 'DefNode', params: { contents: { requireds: { size: { gte: 2 } } } } }, adapter: adapter 141 | ) 142 | expect(rules.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 143 | end 144 | 145 | it 'matches gt' do 146 | rules = described_class.new( 147 | { node_type: 'DefNode', params: { contents: { requireds: { size: { ge: 2 } } } } }, 148 | adapter: adapter 149 | ) 150 | expect(rules.query_nodes(node)).to eq [] 151 | end 152 | 153 | it 'matches lte' do 154 | rules = described_class.new( 155 | { node_type: 'DefNode', params: { contents: { requireds: { size: { lte: 2 } } } } }, adapter: adapter 156 | ) 157 | expect(rules.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 158 | end 159 | 160 | it 'matches lt' do 161 | rules = described_class.new( 162 | { node_type: 'DefNode', params: { contents: { requireds: { size: { lt: 2 } } } } }, 163 | adapter: adapter 164 | ) 165 | expect(rules.query_nodes(node)).to eq [] 166 | end 167 | 168 | it 'matches arguments' do 169 | rules = described_class.new( 170 | { 171 | node_type: 'CallNode', 172 | arguments: { 173 | arguments: { 174 | parts: { 175 | size: 2, 176 | first: { node_type: 'Int' }, 177 | last: { node_type: 'StringLiteral' } 178 | } 179 | } 180 | } 181 | }, 182 | adapter: adapter 183 | ) 184 | expect(rules.query_nodes(node)).to eq [node.body.last.value] 185 | 186 | rules = described_class.new( 187 | { 188 | node_type: 'CallNode', 189 | arguments: { 190 | arguments: { 191 | parts: { 192 | size: 2, 193 | '0': { node_type: 'Int' }, 194 | '-1': { node_type: 'StringLiteral' } 195 | } 196 | } 197 | } 198 | }, 199 | adapter: adapter 200 | ) 201 | expect(rules.query_nodes(node)).to eq [node.body.last.value] 202 | end 203 | 204 | it 'matches evaluated value' do 205 | rules = described_class.new({ node_type: 'Assign', target: '@{{value}}' }, adapter: adapter) 206 | expect(rules.query_nodes(node)).to eq node.body.first.bodystmt.statements.body.first.bodystmt.statements.body 207 | end 208 | 209 | it 'matches evaluated value from base node' do 210 | rules = described_class.new( 211 | { 212 | node_type: 'DefNode', 213 | name: 'initialize', 214 | bodystmt: { statements: { body: { '0': { target: "@{{bodystmt.statements.body.0.value}}" } } } } 215 | }, 216 | adapter: adapter 217 | ) 218 | expect(rules.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 219 | end 220 | 221 | it 'matches empty string' do 222 | node = syntax_tree_parse("call('')") 223 | rules = described_class.new( 224 | { 225 | node_type: 'CallNode', 226 | message: :call, 227 | arguments: { arguments: { parts: { first: '' } } } 228 | }, 229 | adapter: adapter 230 | ) 231 | expect(rules.query_nodes(node)).to eq [node.body.first] 232 | end 233 | 234 | it 'matches hash value' do 235 | node = syntax_tree_parse("{ foo: 'bar' }") 236 | rules = described_class.new({ node_type: 'HashLiteral', foo_value: 'bar' }, adapter: adapter) 237 | expect(rules.query_nodes(node)).to eq [node.body.first] 238 | end 239 | end 240 | end 241 | end 242 | -------------------------------------------------------------------------------- /spec/node_query_lexer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe NodeQueryLexer do 4 | let(:lexer) { described_class.new } 5 | 6 | def assert_tokens(source, expected_tokens) 7 | lexer.parse(source) 8 | tokens = [] 9 | while token = lexer.next_token 10 | tokens << token 11 | end 12 | expect(tokens).to eq expected_tokens 13 | end 14 | 15 | context 'ast node type' do 16 | it 'matches node type' do 17 | source = '.send' 18 | expected_tokens = [[:tNODE_TYPE, "send"]] 19 | assert_tokens source, expected_tokens 20 | end 21 | end 22 | 23 | context 'attribute value' do 24 | it 'matches =' do 25 | source = '.send[message=create]' 26 | expected_tokens = [ 27 | [:tNODE_TYPE, "send"], 28 | [:tOPEN_ATTRIBUTE, "["], 29 | [:tKEY, "message"], 30 | [:tOPERATOR, "="], 31 | [:tIDENTIFIER_VALUE, "create"], 32 | [:tCLOSE_ATTRIBUTE, "]"] 33 | ] 34 | assert_tokens source, expected_tokens 35 | end 36 | 37 | it 'matches nil' do 38 | source = '.send[receiver=nil]' 39 | expected_tokens = [ 40 | [:tNODE_TYPE, "send"], 41 | [:tOPEN_ATTRIBUTE, "["], 42 | [:tKEY, "receiver"], 43 | [:tOPERATOR, "="], 44 | [:tNIL, nil], 45 | [:tCLOSE_ATTRIBUTE, "]"] 46 | ] 47 | assert_tokens source, expected_tokens 48 | end 49 | 50 | it 'matches string' do 51 | source = '.send[message="create"]' 52 | expected_tokens = [ 53 | [:tNODE_TYPE, "send"], 54 | [:tOPEN_ATTRIBUTE, "["], 55 | [:tKEY, "message"], 56 | [:tOPERATOR, "="], 57 | [:tSTRING, "create"], 58 | [:tCLOSE_ATTRIBUTE, "]"] 59 | ] 60 | assert_tokens source, expected_tokens 61 | end 62 | 63 | it 'matches "[]"' do 64 | source = '.send[message="[]"]' 65 | expected_tokens = [ 66 | [:tNODE_TYPE, "send"], 67 | [:tOPEN_ATTRIBUTE, "["], 68 | [:tKEY, "message"], 69 | [:tOPERATOR, "="], 70 | [:tSTRING, "[]"], 71 | [:tCLOSE_ATTRIBUTE, "]"] 72 | ] 73 | assert_tokens source, expected_tokens 74 | end 75 | 76 | it 'matches symbol' do 77 | source = '.send[message=:create]' 78 | expected_tokens = [ 79 | [:tNODE_TYPE, "send"], 80 | [:tOPEN_ATTRIBUTE, "["], 81 | [:tKEY, "message"], 82 | [:tOPERATOR, "="], 83 | [:tSYMBOL, :create], 84 | [:tCLOSE_ATTRIBUTE, "]"] 85 | ] 86 | assert_tokens source, expected_tokens 87 | end 88 | 89 | it 'matches integer' do 90 | source = '[value=1]' 91 | expected_tokens = [ 92 | [:tOPEN_ATTRIBUTE, "["], 93 | [:tKEY, "value"], 94 | [:tOPERATOR, "="], 95 | [:tINTEGER, 1], 96 | [:tCLOSE_ATTRIBUTE, "]"] 97 | ] 98 | assert_tokens source, expected_tokens 99 | end 100 | 101 | it 'matches negative integer' do 102 | source = '[value=-1]' 103 | expected_tokens = [ 104 | [:tOPEN_ATTRIBUTE, "["], 105 | [:tKEY, "value"], 106 | [:tOPERATOR, "="], 107 | [:tINTEGER, -1], 108 | [:tCLOSE_ATTRIBUTE, "]"] 109 | ] 110 | assert_tokens source, expected_tokens 111 | end 112 | 113 | it 'matches float' do 114 | source = '.send[value=1.1]' 115 | expected_tokens = [ 116 | [:tNODE_TYPE, "send"], 117 | [:tOPEN_ATTRIBUTE, "["], 118 | [:tKEY, "value"], 119 | [:tOPERATOR, "="], 120 | [:tFLOAT, 1.1], 121 | [:tCLOSE_ATTRIBUTE, "]"] 122 | ] 123 | assert_tokens source, expected_tokens 124 | end 125 | 126 | it 'matches negative float' do 127 | source = '.send[value=-1.1]' 128 | expected_tokens = [ 129 | [:tNODE_TYPE, "send"], 130 | [:tOPEN_ATTRIBUTE, "["], 131 | [:tKEY, "value"], 132 | [:tOPERATOR, "="], 133 | [:tFLOAT, -1.1], 134 | [:tCLOSE_ATTRIBUTE, "]"] 135 | ] 136 | assert_tokens source, expected_tokens 137 | end 138 | 139 | it 'matches boolean' do 140 | source = '.send[value=true]' 141 | expected_tokens = [ 142 | [:tNODE_TYPE, "send"], 143 | [:tOPEN_ATTRIBUTE, "["], 144 | [:tKEY, "value"], 145 | [:tOPERATOR, "="], 146 | [:tBOOLEAN, true], 147 | [:tCLOSE_ATTRIBUTE, "]"] 148 | ] 149 | assert_tokens source, expected_tokens 150 | end 151 | 152 | it 'identifier can contain !' do 153 | source = '.send[message=create!]' 154 | expected_tokens = [ 155 | [:tNODE_TYPE, "send"], 156 | [:tOPEN_ATTRIBUTE, "["], 157 | [:tKEY, "message"], 158 | [:tOPERATOR, "="], 159 | [:tIDENTIFIER_VALUE, "create!"], 160 | [:tCLOSE_ATTRIBUTE, "]"] 161 | ] 162 | assert_tokens source, expected_tokens 163 | end 164 | 165 | it 'identifier can contain ?' do 166 | source = '.send[message=empty?]' 167 | expected_tokens = [ 168 | [:tNODE_TYPE, "send"], 169 | [:tOPEN_ATTRIBUTE, "["], 170 | [:tKEY, "message"], 171 | [:tOPERATOR, "="], 172 | [:tIDENTIFIER_VALUE, "empty?"], 173 | [:tCLOSE_ATTRIBUTE, "]"] 174 | ] 175 | assert_tokens source, expected_tokens 176 | end 177 | 178 | it 'identifier can contain <, >, =' do 179 | source = '.send[message=<]' 180 | expected_tokens = [ 181 | [:tNODE_TYPE, "send"], 182 | [:tOPEN_ATTRIBUTE, "["], 183 | [:tKEY, "message"], 184 | [:tOPERATOR, "="], 185 | [:tIDENTIFIER_VALUE, "<"], 186 | [:tCLOSE_ATTRIBUTE, "]"] 187 | ] 188 | assert_tokens source, expected_tokens 189 | 190 | source = '.send[message==]' 191 | expected_tokens = [ 192 | [:tNODE_TYPE, "send"], 193 | [:tOPEN_ATTRIBUTE, "["], 194 | [:tKEY, "message"], 195 | [:tOPERATOR, "="], 196 | [:tIDENTIFIER_VALUE, "="], 197 | [:tCLOSE_ATTRIBUTE, "]"] 198 | ] 199 | assert_tokens source, expected_tokens 200 | 201 | source = '.send[message=>]' 202 | expected_tokens = [ 203 | [:tNODE_TYPE, "send"], 204 | [:tOPEN_ATTRIBUTE, "["], 205 | [:tKEY, "message"], 206 | [:tOPERATOR, "="], 207 | [:tIDENTIFIER_VALUE, ">"], 208 | [:tCLOSE_ATTRIBUTE, "]"] 209 | ] 210 | assert_tokens source, expected_tokens 211 | end 212 | 213 | it 'matches empty string' do 214 | source = ".send[arguments.first='']" 215 | expected_tokens = [ 216 | [:tNODE_TYPE, "send"], 217 | [:tOPEN_ATTRIBUTE, "["], 218 | [:tKEY, "arguments.first"], 219 | [:tOPERATOR, "="], 220 | [:tSTRING, ""], 221 | [:tCLOSE_ATTRIBUTE, "]"] 222 | ] 223 | assert_tokens source, expected_tokens 224 | end 225 | 226 | it 'matches :[] message' do 227 | source = ".send[message=[]]" 228 | expected_tokens = [ 229 | [:tNODE_TYPE, "send"], 230 | [:tOPEN_ATTRIBUTE, "["], 231 | [:tKEY, "message"], 232 | [:tOPERATOR, "="], 233 | [:tIDENTIFIER_VALUE, "[]"], 234 | [:tCLOSE_ATTRIBUTE, "]"] 235 | ] 236 | assert_tokens source, expected_tokens 237 | end 238 | 239 | it 'matches :[] message' do 240 | source = ".send[message=:[]=]" 241 | expected_tokens = [ 242 | [:tNODE_TYPE, "send"], 243 | [:tOPEN_ATTRIBUTE, "["], 244 | [:tKEY, "message"], 245 | [:tOPERATOR, "="], 246 | [:tSYMBOL, :[]=], 247 | [:tCLOSE_ATTRIBUTE, "]"] 248 | ] 249 | assert_tokens source, expected_tokens 250 | end 251 | 252 | it 'matches nil?' do 253 | source = ".send[message=nil?]" 254 | expected_tokens = [ 255 | [:tNODE_TYPE, "send"], 256 | [:tOPEN_ATTRIBUTE, "["], 257 | [:tKEY, "message"], 258 | [:tOPERATOR, "="], 259 | [:tIDENTIFIER_VALUE, "nil?"], 260 | [:tCLOSE_ATTRIBUTE, "]"] 261 | ] 262 | assert_tokens source, expected_tokens 263 | end 264 | 265 | it 'matches evaluated value' do 266 | source = '.pair[key="{{value}}"]' 267 | expected_tokens = [ 268 | [:tNODE_TYPE, "pair"], 269 | [:tOPEN_ATTRIBUTE, "["], 270 | [:tKEY, "key"], 271 | [:tOPERATOR, "="], 272 | [:tSTRING, "{{value}}"], 273 | [:tCLOSE_ATTRIBUTE, "]"] 274 | ] 275 | assert_tokens source, expected_tokens 276 | end 277 | 278 | it 'matches evaluated value with identifier value' do 279 | source = '.ivasgn[variable="@{{value}}"]' 280 | expected_tokens = [ 281 | [:tNODE_TYPE, "ivasgn"], 282 | [:tOPEN_ATTRIBUTE, "["], 283 | [:tKEY, "variable"], 284 | [:tOPERATOR, "="], 285 | [:tSTRING, "@{{value}}"], 286 | [:tCLOSE_ATTRIBUTE, "]"] 287 | ] 288 | assert_tokens source, expected_tokens 289 | end 290 | 291 | it 'matches nested value' do 292 | source = <<~EOS 293 | .send[ 294 | receiver= 295 | .send[message=:create] 296 | ] 297 | EOS 298 | expected_tokens = [ 299 | [:tNODE_TYPE, "send"], 300 | [:tOPEN_ATTRIBUTE, "["], 301 | [:tKEY, "receiver"], 302 | [:tOPERATOR, "="], 303 | [:tNODE_TYPE, "send"], 304 | [:tOPEN_ATTRIBUTE, "["], 305 | [:tKEY, "message"], 306 | [:tOPERATOR, "="], 307 | [:tSYMBOL, :create], 308 | [:tCLOSE_ATTRIBUTE, "]"], 309 | [:tCLOSE_ATTRIBUTE, "]"] 310 | ] 311 | assert_tokens source, expected_tokens 312 | end 313 | 314 | it 'matches deep nested value' do 315 | source = <<~EOS 316 | .send[ 317 | arguments=[size=2][first=.str][last=.str] 318 | ] 319 | EOS 320 | expected_tokens = [ 321 | [:tNODE_TYPE, "send"], 322 | [:tOPEN_ATTRIBUTE, "["], 323 | [:tKEY, "arguments"], 324 | [:tOPERATOR, "="], 325 | [:tOPEN_ATTRIBUTE, "["], 326 | [:tKEY, "size"], 327 | [:tOPERATOR, "="], 328 | [:tINTEGER, 2], 329 | [:tCLOSE_ATTRIBUTE, "]"], 330 | [:tOPEN_ATTRIBUTE, "["], 331 | [:tKEY, "first"], 332 | [:tOPERATOR, "="], 333 | [:tNODE_TYPE, "str"], 334 | [:tCLOSE_ATTRIBUTE, "]"], 335 | [:tOPEN_ATTRIBUTE, "["], 336 | [:tKEY, "last"], 337 | [:tOPERATOR, "="], 338 | [:tNODE_TYPE, "str"], 339 | [:tCLOSE_ATTRIBUTE, "]"], 340 | [:tCLOSE_ATTRIBUTE, "]"] 341 | ] 342 | assert_tokens source, expected_tokens 343 | end 344 | end 345 | 346 | context 'attribute condition' do 347 | it 'matches !=' do 348 | source = '.send[message != create]' 349 | expected_tokens = [ 350 | [:tNODE_TYPE, "send"], 351 | [:tOPEN_ATTRIBUTE, "["], 352 | [:tKEY, "message"], 353 | [:tOPERATOR, "!="], 354 | [:tIDENTIFIER_VALUE, "create"], 355 | [:tCLOSE_ATTRIBUTE, "]"] 356 | ] 357 | assert_tokens source, expected_tokens 358 | end 359 | 360 | it 'matches >' do 361 | source = '[value > 1]' 362 | expected_tokens = [ 363 | [:tOPEN_ATTRIBUTE, "["], 364 | [:tKEY, "value"], 365 | [:tOPERATOR, ">"], 366 | [:tINTEGER, 1], 367 | [:tCLOSE_ATTRIBUTE, "]"] 368 | ] 369 | assert_tokens source, expected_tokens 370 | end 371 | 372 | it 'matches <' do 373 | source = '[value < 1]' 374 | expected_tokens = [ 375 | [:tOPEN_ATTRIBUTE, "["], 376 | [:tKEY, "value"], 377 | [:tOPERATOR, "<"], 378 | [:tINTEGER, 1], 379 | [:tCLOSE_ATTRIBUTE, "]"] 380 | ] 381 | assert_tokens source, expected_tokens 382 | end 383 | 384 | it 'matches >=' do 385 | source = '[value >= 1]' 386 | expected_tokens = [ 387 | [:tOPEN_ATTRIBUTE, "["], 388 | [:tKEY, "value"], 389 | [:tOPERATOR, ">="], 390 | [:tINTEGER, 1], 391 | [:tCLOSE_ATTRIBUTE, "]"] 392 | ] 393 | assert_tokens source, expected_tokens 394 | end 395 | 396 | it 'matches <=' do 397 | source = '[value <= 1]' 398 | expected_tokens = [ 399 | [:tOPEN_ATTRIBUTE, "["], 400 | [:tKEY, "value"], 401 | [:tOPERATOR, "<="], 402 | [:tINTEGER, 1], 403 | [:tCLOSE_ATTRIBUTE, "]"] 404 | ] 405 | assert_tokens source, expected_tokens 406 | end 407 | 408 | it 'matches =~' do 409 | source = '.send[message=~/create/i]' 410 | expected_tokens = [ 411 | [:tNODE_TYPE, "send"], 412 | [:tOPEN_ATTRIBUTE, "["], 413 | [:tKEY, "message"], 414 | [:tOPERATOR, "=~"], 415 | [:tREGEXP, /create/i], 416 | [:tCLOSE_ATTRIBUTE, "]"] 417 | ] 418 | assert_tokens source, expected_tokens 419 | end 420 | 421 | it 'matches !~' do 422 | source = '.send[message!~/create/i]' 423 | expected_tokens = [ 424 | [:tNODE_TYPE, "send"], 425 | [:tOPEN_ATTRIBUTE, "["], 426 | [:tKEY, "message"], 427 | [:tOPERATOR, "!~"], 428 | [:tREGEXP, /create/i], 429 | [:tCLOSE_ATTRIBUTE, "]"] 430 | ] 431 | assert_tokens source, expected_tokens 432 | end 433 | 434 | it 'matche empty array' do 435 | source = '.send[arguments=()]' 436 | expected_tokens = [ 437 | [:tNODE_TYPE, "send"], 438 | [:tOPEN_ATTRIBUTE, "["], 439 | [:tKEY, "arguments"], 440 | [:tOPERATOR, "="], 441 | [:tOPEN_ARRAY, "("], 442 | [:tCLOSE_ARRAY, ")"], 443 | [:tCLOSE_ATTRIBUTE, "]"] 444 | ] 445 | assert_tokens source, expected_tokens 446 | end 447 | 448 | it 'matche equal array' do 449 | source = '.send[arguments=(:create)]' 450 | expected_tokens = [ 451 | [:tNODE_TYPE, "send"], 452 | [:tOPEN_ATTRIBUTE, "["], 453 | [:tKEY, "arguments"], 454 | [:tOPERATOR, "="], 455 | [:tOPEN_ARRAY, "("], 456 | [:tSYMBOL, :create], 457 | [:tCLOSE_ARRAY, ")"], 458 | [:tCLOSE_ATTRIBUTE, "]"] 459 | ] 460 | assert_tokens source, expected_tokens 461 | end 462 | 463 | it 'matche not equal array' do 464 | source = '.send[arguments!=(:create)]' 465 | expected_tokens = [ 466 | [:tNODE_TYPE, "send"], 467 | [:tOPEN_ATTRIBUTE, "["], 468 | [:tKEY, "arguments"], 469 | [:tOPERATOR, "!="], 470 | [:tOPEN_ARRAY, "("], 471 | [:tSYMBOL, :create], 472 | [:tCLOSE_ARRAY, ")"], 473 | [:tCLOSE_ATTRIBUTE, "]"] 474 | ] 475 | assert_tokens source, expected_tokens 476 | end 477 | 478 | it 'matches IN' do 479 | source = '.send[message IN (create build)]' 480 | expected_tokens = [ 481 | [:tNODE_TYPE, "send"], 482 | [:tOPEN_ATTRIBUTE, "["], 483 | [:tKEY, "message"], 484 | [:tOPERATOR, "in"], 485 | [:tOPEN_ARRAY, "("], 486 | [:tIDENTIFIER_VALUE, "create"], 487 | [:tIDENTIFIER_VALUE, "build"], 488 | [:tCLOSE_ARRAY, ")"], 489 | [:tCLOSE_ATTRIBUTE, "]"] 490 | ] 491 | assert_tokens source, expected_tokens 492 | end 493 | 494 | it 'matches NOT IN' do 495 | source = '.send[message NOT IN (create build)]' 496 | expected_tokens = [ 497 | [:tNODE_TYPE, "send"], 498 | [:tOPEN_ATTRIBUTE, "["], 499 | [:tKEY, "message"], 500 | [:tOPERATOR, "not_in"], 501 | [:tOPEN_ARRAY, "("], 502 | [:tIDENTIFIER_VALUE, "create"], 503 | [:tIDENTIFIER_VALUE, "build"], 504 | [:tCLOSE_ARRAY, ")"], 505 | [:tCLOSE_ATTRIBUTE, "]"] 506 | ] 507 | assert_tokens source, expected_tokens 508 | end 509 | 510 | it 'matches INCLUDES' do 511 | source = '.send[arguments INCLUDES &block]' 512 | expected_tokens = [ 513 | [:tNODE_TYPE, "send"], 514 | [:tOPEN_ATTRIBUTE, "["], 515 | [:tKEY, "arguments"], 516 | [:tOPERATOR, "includes"], 517 | [:tIDENTIFIER_VALUE, "&block"], 518 | [:tCLOSE_ATTRIBUTE, "]"] 519 | ] 520 | assert_tokens source, expected_tokens 521 | end 522 | 523 | it 'matches NOT INCLUDES' do 524 | source = '.send[arguments NOT INCLUDES &block]' 525 | expected_tokens = [ 526 | [:tNODE_TYPE, "send"], 527 | [:tOPEN_ATTRIBUTE, "["], 528 | [:tKEY, "arguments"], 529 | [:tOPERATOR, "not_includes"], 530 | [:tIDENTIFIER_VALUE, "&block"], 531 | [:tCLOSE_ATTRIBUTE, "]"] 532 | ] 533 | assert_tokens source, expected_tokens 534 | end 535 | 536 | it 'matches * in attribute key' do 537 | source = '.def[arguments.*.name IN (foo bar)]' 538 | expected_tokens = [ 539 | [:tNODE_TYPE, "def"], 540 | [:tOPEN_ATTRIBUTE, "["], 541 | [:tKEY, "arguments.*.name"], 542 | [:tOPERATOR, "in"], 543 | [:tOPEN_ARRAY, "("], 544 | [:tIDENTIFIER_VALUE, "foo"], 545 | [:tIDENTIFIER_VALUE, "bar"], 546 | [:tCLOSE_ARRAY, ")"], 547 | [:tCLOSE_ATTRIBUTE, "]"] 548 | ] 549 | assert_tokens source, expected_tokens 550 | end 551 | end 552 | 553 | context 'nested attribute' do 554 | it 'matches' do 555 | source = '.send[receiver.message=:create]' 556 | expected_tokens = [ 557 | [:tNODE_TYPE, "send"], 558 | [:tOPEN_ATTRIBUTE, "["], 559 | [:tKEY, "receiver.message"], 560 | [:tOPERATOR, "="], 561 | [:tSYMBOL, :create], 562 | [:tCLOSE_ATTRIBUTE, "]"] 563 | ] 564 | assert_tokens source, expected_tokens 565 | end 566 | end 567 | 568 | context 'descendant' do 569 | it 'matches' do 570 | source = '.class .send' 571 | expected_tokens = [[:tNODE_TYPE, "class"], [:tNODE_TYPE, "send"]] 572 | assert_tokens source, expected_tokens 573 | end 574 | end 575 | 576 | context 'child' do 577 | it 'matches' do 578 | source = '.def > .send' 579 | expected_tokens = [[:tNODE_TYPE, "def"], [:tRELATIONSHIP, ">"], [:tNODE_TYPE, "send"]] 580 | assert_tokens source, expected_tokens 581 | end 582 | end 583 | 584 | context 'subsequent sibling' do 585 | it 'matches' do 586 | source = '.send ~ .send' 587 | expected_tokens = [[:tNODE_TYPE, "send"], [:tRELATIONSHIP, "~"], [:tNODE_TYPE, "send"]] 588 | assert_tokens source, expected_tokens 589 | end 590 | end 591 | 592 | context 'next sibling' do 593 | it 'matches' do 594 | source = '.send + .send' 595 | expected_tokens = [[:tNODE_TYPE, "send"], [:tRELATIONSHIP, "+"], [:tNODE_TYPE, "send"]] 596 | assert_tokens source, expected_tokens 597 | end 598 | end 599 | 600 | context ':has' do 601 | it 'matches' do 602 | source = '.class:has(> .def)' 603 | expected_tokens = [ 604 | [:tNODE_TYPE, "class"], 605 | [:tPSEUDO_CLASS, "has"], 606 | [:tOPEN_SELECTOR, "("], 607 | [:tRELATIONSHIP, ">"], 608 | [:tNODE_TYPE, "def"], 609 | [:tCLOSE_SELECTOR, ")"] 610 | ] 611 | assert_tokens source, expected_tokens 612 | end 613 | end 614 | 615 | context ':not_has' do 616 | it 'matches' do 617 | source = '.class:not_has(> .def)' 618 | expected_tokens = [ 619 | [:tNODE_TYPE, "class"], 620 | [:tPSEUDO_CLASS, "not_has"], 621 | [:tOPEN_SELECTOR, "("], 622 | [:tRELATIONSHIP, ">"], 623 | [:tNODE_TYPE, "def"], 624 | [:tCLOSE_SELECTOR, ")"] 625 | ] 626 | assert_tokens source, expected_tokens 627 | end 628 | end 629 | 630 | context 'goto_scope' do 631 | it 'matches' do 632 | source = '.block body > .def' 633 | expected_tokens = [[:tNODE_TYPE, "block"], [:tGOTO_SCOPE, "body"], [:tRELATIONSHIP, ">"], [:tNODE_TYPE, "def"]] 634 | assert_tokens source, expected_tokens 635 | end 636 | end 637 | end 638 | -------------------------------------------------------------------------------- /spec/node_query_parser/parser_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'oedipus_lex' 3 | 4 | RSpec.describe NodeQueryParser do 5 | let(:parser) { described_class.new(adapter: NodeQuery::ParserAdapter.new) } 6 | 7 | describe '#query_nodes' do 8 | context 'parser' do 9 | let(:node) { 10 | parser_parse(<<~EOS) 11 | class User < Base 12 | def initialize(id, name) 13 | @id = id 14 | @name = name 15 | end 16 | end 17 | 18 | user = User.new(1, "Murphy") 19 | EOS 20 | } 21 | 22 | it 'matches node type' do 23 | expression = parser.parse('.def') 24 | expect(expression.query_nodes(node)).to eq node.body.first.body 25 | end 26 | 27 | it 'matches node type and one attribute' do 28 | expression = parser.parse('.class[name=User]') 29 | expect(expression.query_nodes(node)).to eq [node.body.first] 30 | end 31 | 32 | it 'matches nested attribute' do 33 | expression = parser.parse('.class[parent_class.name=Base]') 34 | expect(expression.query_nodes(node)).to eq [node.body.first] 35 | end 36 | 37 | it 'matches method result' do 38 | expression = parser.parse('.def[arguments.size=2]') 39 | expect(expression.query_nodes(node)).to eq node.body.first.body 40 | end 41 | 42 | it 'matches multiple attributes' do 43 | expression = parser.parse('.def[arguments.size=2][arguments.0=id][arguments.1=name]') 44 | expect(expression.query_nodes(node)).to eq node.body.first.body 45 | end 46 | 47 | it 'matches ^=' do 48 | expression = parser.parse('.def[name^=init]') 49 | expect(expression.query_nodes(node)).to eq node.body.first.body 50 | end 51 | 52 | it 'matches $=' do 53 | expression = parser.parse('.def[name$=ize]') 54 | expect(expression.query_nodes(node)).to eq node.body.first.body 55 | end 56 | 57 | it 'matches *=' do 58 | expression = parser.parse('.def[name*=ial]') 59 | expect(expression.query_nodes(node)).to eq node.body.first.body 60 | end 61 | 62 | it 'matches !=' do 63 | expression = parser.parse('.def[name!=foobar]') 64 | expect(expression.query_nodes(node)).to eq node.body.first.body 65 | end 66 | 67 | it 'matches =~' do 68 | expression = parser.parse('.def[name=~/init/]') 69 | expect(expression.query_nodes(node)).to eq node.body.first.body 70 | end 71 | 72 | it 'matches !~' do 73 | expression = parser.parse('.def[name!~/bar/]') 74 | expect(expression.query_nodes(node)).to eq node.body.first.body 75 | end 76 | 77 | it 'matches in' do 78 | expression = parser.parse('.ivasgn[variable IN (@id @name)]') 79 | expect(expression.query_nodes(node)).to eq node.body.first.body.first.body 80 | end 81 | 82 | it 'matches not in' do 83 | expression = parser.parse('.ivasgn[variable NOT IN (@id @name)]') 84 | expect(expression.query_nodes(node)).to eq [] 85 | end 86 | 87 | it 'matches includes' do 88 | expression = parser.parse('.def[arguments INCLUDES id]') 89 | expect(expression.query_nodes(node)).to eq node.body.first.body 90 | end 91 | 92 | it 'matches includes with selector' do 93 | expression = parser.parse('.def[arguments INCLUDES .arg[name=id]]') 94 | expect(expression.query_nodes(node)).to eq node.body.first.body 95 | end 96 | 97 | it 'matches not includes' do 98 | expression = parser.parse('.def[arguments NOT INCLUDES foobar]') 99 | expect(expression.query_nodes(node)).to eq node.body.first.body 100 | end 101 | 102 | it 'matches not includes with selector' do 103 | expression = parser.parse('.def[arguments NOT INCLUDES .arg[name=foobar]]') 104 | expect(expression.query_nodes(node)).to eq node.body.first.body 105 | end 106 | 107 | it 'matches equal array' do 108 | expression = parser.parse('.def[arguments=(id name)]') 109 | expect(expression.query_nodes(node)).to eq node.body.first.body 110 | 111 | expression = parser.parse('.def[arguments=(name id)]') 112 | expect(expression.query_nodes(node)).to eq [] 113 | end 114 | 115 | it 'matches not equal array' do 116 | expression = parser.parse('.def[arguments!=(id name)]') 117 | expect(expression.query_nodes(node)).to eq [] 118 | 119 | expression = parser.parse('.def[arguments!=(name id)]') 120 | expect(expression.query_nodes(node)).to eq node.body.first.body 121 | end 122 | 123 | it 'matches nested selector' do 124 | expression = parser.parse('.def[body.0=.ivasgn]') 125 | expect(expression.query_nodes(node)).to eq node.body.first.body 126 | end 127 | 128 | it 'matches * in attribute key' do 129 | expression = parser.parse('.def[arguments.*.name=(id name)]') 130 | expect(expression.query_nodes(node)).to eq node.body.first.body 131 | end 132 | 133 | it 'matches descendant node' do 134 | expression = parser.parse('.class .ivasgn[variable=@id]') 135 | expect(expression.query_nodes(node)).to eq [node.body.first.body.first.body.first] 136 | end 137 | 138 | it 'matches three level descendant node' do 139 | expression = parser.parse('.class .def .ivasgn[variable=@id]') 140 | expect(expression.query_nodes(node)).to eq [node.body.first.body.first.body.first] 141 | end 142 | 143 | it 'matches child node' do 144 | expression = parser.parse('.def > .ivasgn[variable=@id]') 145 | expect(expression.query_nodes(node)).to eq [node.body.first.body.first.body.first] 146 | end 147 | 148 | it 'matches next sibling node' do 149 | expression = parser.parse('.ivasgn[variable=@id] + .ivasgn[variable=@name]') 150 | expect(expression.query_nodes(node)).to eq [node.body.first.body.first.body[1]] 151 | end 152 | 153 | it 'matches sebsequent sibling node' do 154 | expression = parser.parse('.ivasgn[variable=@id] ~ .ivasgn[variable=@name]') 155 | expect(expression.query_nodes(node)).to eq [node.body.first.body.first.body[1]] 156 | end 157 | 158 | it 'matches goto scope' do 159 | expression = parser.parse('.def body > .ivasgn[variable=@id]') 160 | expect(expression.query_nodes(node)).to eq [node.body.first.body.first.body.first] 161 | 162 | expression = parser.parse('.def body .ivasgn[variable=@id]') 163 | expect(expression.query_nodes(node)).to eq [node.body.first.body.first.body.first] 164 | end 165 | 166 | it 'matches multiple goto scope' do 167 | node = parser_parse("RSpec.describe User do\nend") 168 | expression = parser.parse('.block caller.arguments .const[name=User]') 169 | expect(expression.query_nodes(node)).to eq [node.caller.arguments.first] 170 | end 171 | 172 | it 'matches has selector' do 173 | expression = parser.parse('.def:has(> .ivasgn[variable=@id])') 174 | expect(expression.query_nodes(node)).to eq [node.body.first.body.first] 175 | end 176 | 177 | it 'matches not_has selector' do 178 | expression = parser.parse('.def:not_has(> .ivasgn[variable=@id])') 179 | expect(expression.query_nodes(node)).to eq [] 180 | end 181 | 182 | it 'matches root has selector' do 183 | expression = parser.parse(':has(> .class)') 184 | expect(expression.query_nodes(node)).to eq [node] 185 | end 186 | 187 | it 'matches root not_has selector' do 188 | expression = parser.parse(':not_has(> .class)') 189 | expect(expression.query_nodes(node)).to eq [] 190 | end 191 | 192 | it 'matches >=' do 193 | expression = parser.parse('.def[arguments.size>=2]') 194 | expect(expression.query_nodes(node)).to eq node.body.first.body 195 | end 196 | 197 | it 'matches >' do 198 | expression = parser.parse('.def[arguments.size>2]') 199 | expect(expression.query_nodes(node)).to eq [] 200 | end 201 | 202 | it 'matches <=' do 203 | expression = parser.parse('.def[arguments.size<=2]') 204 | expect(expression.query_nodes(node)).to eq node.body.first.body 205 | end 206 | 207 | it 'matches <' do 208 | expression = parser.parse('.def[arguments.size<2]') 209 | expect(expression.query_nodes(node)).to eq [] 210 | end 211 | 212 | it 'matches arguments' do 213 | expression = parser.parse('.send[arguments.size=2][arguments.first=.int][arguments.last=.str]') 214 | expect(expression.query_nodes(node)).to eq [node.body.last.value] 215 | 216 | expression = parser.parse('.send[arguments.size=2][arguments.0=.int][arguments.-1=.str]') 217 | expect(expression.query_nodes(node)).to eq [node.body.last.value] 218 | end 219 | 220 | it 'matches evaluated value' do 221 | expression = parser.parse('.ivasgn[variable="@{{value}}"]') 222 | expect(expression.query_nodes(node)).to eq node.body.first.body.first.body 223 | end 224 | 225 | it 'matches evaluated value from base node' do 226 | expression = parser.parse('.def[name=initialize][body.0.variable="@{{body.0.value}}"]') 227 | expect(expression.query_nodes(node)).to eq node.body.first.body 228 | end 229 | 230 | it 'matches ,' do 231 | expression = parser.parse('.ivasgn[variable=@id], .ivasgn[variable=@name]') 232 | expect(expression.query_nodes(node)).to eq node.body.first.body.first.body 233 | end 234 | 235 | it 'matches :first-child' do 236 | expression = parser.parse('.ivasgn:first-child') 237 | expect(expression.query_nodes(node)).to eq [node.body.first.body.first.body.first] 238 | 239 | expression = parser.parse('.def > .ivasgn:first-child') 240 | expect(expression.query_nodes(node)).to eq [node.body.first.body.first.body.first] 241 | 242 | expression = parser.parse('.block:first-child') 243 | expect(expression.query_nodes(node)).to eq [] 244 | end 245 | 246 | it 'matches :last-child' do 247 | expression = parser.parse('.ivasgn:last-child') 248 | expect(expression.query_nodes(node)).to eq [node.body.first.body.first.body.last] 249 | 250 | expression = parser.parse('.def > .ivasgn:last-child') 251 | expect(expression.query_nodes(node)).to eq [node.body.first.body.first.body.last] 252 | 253 | expression = parser.parse('.block:last-child') 254 | expect(expression.query_nodes(node)).to eq [] 255 | end 256 | 257 | it 'matches []' do 258 | node = parser_parse("user[:error]") 259 | expression = parser.parse('.send[message=[]]') 260 | expect(expression.query_nodes(node)).to eq [node] 261 | end 262 | 263 | it 'matches []=' do 264 | node = parser_parse("user[:error] = 'error'") 265 | expression = parser.parse('.send[message=:[]=]') 266 | expect(expression.query_nodes(node)).to eq [node] 267 | end 268 | 269 | it 'matches nil and nil?' do 270 | node = parser_parse("nil.nil?") 271 | expression = parser.parse('.send[receiver=nil][message=nil?]') 272 | expect(expression.query_nodes(node)).to eq [node] 273 | end 274 | 275 | it 'matches empty string' do 276 | node = parser_parse("call('')") 277 | expression = parser.parse('.send[message=call][arguments.first=""]') 278 | expect(expression.query_nodes(node)).to eq [node] 279 | end 280 | 281 | it 'matches hash value' do 282 | node = parser_parse("{ foo: 'bar' }") 283 | expression = parser.parse(".hash[foo_value='bar']") 284 | expect(expression.query_nodes(node)).to eq [node] 285 | end 286 | 287 | it 'sets option including_self to false' do 288 | expression = parser.parse('.class') 289 | expect(expression.query_nodes(node.children.first, { including_self: false })).to eq [] 290 | 291 | expect(expression.query_nodes(node.children.first)).to eq [node.children.first] 292 | end 293 | 294 | it 'sets options stop_at_first_match to true' do 295 | expression = parser.parse('.ivasgn') 296 | expect( 297 | expression.query_nodes( 298 | node.children.first, 299 | { stop_at_first_match: true } 300 | ) 301 | ).to eq [node.children.first.body.first.body.first] 302 | 303 | expect(expression.query_nodes(node.children.first)).to eq node.children.first.body.first.body 304 | end 305 | 306 | it 'sets options recursive to false' do 307 | expression = parser.parse('.def') 308 | expect(expression.query_nodes(node, { recursive: false })).to eq [] 309 | 310 | expect(expression.query_nodes(node)).to eq [node.children.first.body.first] 311 | end 312 | end 313 | end 314 | end 315 | -------------------------------------------------------------------------------- /spec/node_query_parser/prism_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'oedipus_lex' 3 | 4 | RSpec.describe NodeQueryParser do 5 | let(:parser) { described_class.new(adapter: NodeQuery::PrismAdapter.new) } 6 | 7 | describe '#query_nodes' do 8 | context 'prism' do 9 | let(:node) { 10 | prism_parse(<<~EOS) 11 | class User < Base 12 | def initialize(id, name) 13 | @id = id 14 | @name = name 15 | end 16 | end 17 | 18 | user = User.new(1, "Murphy") 19 | EOS 20 | } 21 | 22 | it 'matches node type' do 23 | expression = parser.parse('.def_node') 24 | expect(expression.query_nodes(node)).to eq [node.body.first.body.body.first] 25 | end 26 | 27 | it 'matches node type and one attribute' do 28 | expression = parser.parse('.class_node[constant_path=User]') 29 | expect(expression.query_nodes(node)).to eq [node.body.first] 30 | end 31 | 32 | it 'matches nested attribute' do 33 | expression = parser.parse('.class_node[superclass.name=Base]') 34 | expect(expression.query_nodes(node)).to eq [node.body.first] 35 | end 36 | 37 | it 'matches method result' do 38 | expression = parser.parse('.def_node[parameters.requireds.size=2]') 39 | expect(expression.query_nodes(node)).to eq [node.body.first.body.body.first] 40 | end 41 | 42 | it 'matches multiple attributes' do 43 | expression = parser.parse('.def_node[parameters.requireds.size=2][parameters.requireds.0=id][parameters.requireds.1=name]') 44 | expect(expression.query_nodes(node)).to eq [node.body.first.body.body.first] 45 | end 46 | 47 | it 'matches ^=' do 48 | expression = parser.parse('.def_node[name^=init]') 49 | expect(expression.query_nodes(node)).to eq [node.body.first.body.body.first] 50 | end 51 | 52 | it 'matches $=' do 53 | expression = parser.parse('.def_node[name$=ize]') 54 | expect(expression.query_nodes(node)).to eq [node.body.first.body.body.first] 55 | end 56 | 57 | it 'matches *=' do 58 | expression = parser.parse('.def_node[name*=ial]') 59 | expect(expression.query_nodes(node)).to eq [node.body.first.body.body.first] 60 | end 61 | 62 | it 'matches !=' do 63 | expression = parser.parse('.def_node[name!=foobar]') 64 | expect(expression.query_nodes(node)).to eq [node.body.first.body.body.first] 65 | end 66 | 67 | it 'matches =~' do 68 | expression = parser.parse('.def_node[name=~/init/]') 69 | expect(expression.query_nodes(node)).to eq [node.body.first.body.body.first] 70 | end 71 | 72 | it 'matches !~' do 73 | expression = parser.parse('.def_node[name!~/bar/]') 74 | expect(expression.query_nodes(node)).to eq [node.body.first.body.body.first] 75 | end 76 | 77 | it 'matches in' do 78 | expression = parser.parse('.instance_variable_write_node[name IN (@id @name)]') 79 | expect(expression.query_nodes(node)).to eq [ 80 | node.body.first.body.body.first.body.body.first, 81 | node.body.first.body.body.first.body.body.last 82 | ] 83 | end 84 | 85 | it 'matches not in' do 86 | expression = parser.parse('.instance_variable_write_node[name NOT IN (@id @name)]') 87 | expect(expression.query_nodes(node)).to eq [] 88 | end 89 | 90 | it 'matches includes' do 91 | expression = parser.parse('.def_node[parameters.requireds INCLUDES id]') 92 | expect(expression.query_nodes(node)).to eq [node.body.first.body.body.first] 93 | end 94 | 95 | it 'matches includes with selector' do 96 | expression = parser.parse('.def_node[parameters.requireds INCLUDES .required_parameter_node[name=id]]') 97 | expect(expression.query_nodes(node)).to eq [node.body.first.body.body.first] 98 | end 99 | 100 | it 'matches not includes' do 101 | expression = parser.parse('.def_node[parameters.requireds NOT INCLUDES foobar]') 102 | expect(expression.query_nodes(node)).to eq [node.body.first.body.body.first] 103 | end 104 | 105 | it 'matches not includes with selector' do 106 | expression = parser.parse('.def_node[parameters.requireds NOT INCLUDES .required_parameter_node[name=foobar]]') 107 | expect(expression.query_nodes(node)).to eq [node.body.first.body.body.first] 108 | end 109 | 110 | it 'matches equal array' do 111 | expression = parser.parse('.def_node[parameters.requireds=(id name)]') 112 | expect(expression.query_nodes(node)).to eq [node.body.first.body.body.first] 113 | 114 | expression = parser.parse('.def_node[parameters.requireds=(name id)]') 115 | expect(expression.query_nodes(node)).to eq [] 116 | end 117 | 118 | it 'matches not equal array' do 119 | expression = parser.parse('.def_node[parameters.requireds!=(id name)]') 120 | expect(expression.query_nodes(node)).to eq [] 121 | 122 | expression = parser.parse('.def_node[parameters.requireds!=(name id)]') 123 | expect(expression.query_nodes(node)).to eq [node.body.first.body.body.first] 124 | end 125 | 126 | it 'matches nested selector' do 127 | expression = parser.parse('.def_node[body.body.0=.instance_variable_write_node]') 128 | expect(expression.query_nodes(node)).to eq [node.body.first.body.body.first] 129 | end 130 | 131 | it 'matches * in attribute key' do 132 | expression = parser.parse('.def_node[parameters.requireds.*.name=(id name)]') 133 | expect(expression.query_nodes(node)).to eq [node.body.first.body.body.first] 134 | end 135 | 136 | it 'matches descendant node' do 137 | expression = parser.parse('.class_node .instance_variable_write_node[name=@id]') 138 | expect(expression.query_nodes(node)).to eq [node.body.first.body.body.first.body.body.first] 139 | end 140 | 141 | it 'matches three level descendant node' do 142 | expression = parser.parse('.class_node .def_node .instance_variable_write_node[name=@id]') 143 | expect(expression.query_nodes(node)).to eq [node.body.first.body.body.first.body.body.first] 144 | end 145 | 146 | it 'matches child node' do 147 | expression = parser.parse('.instance_variable_write_node > .local_variable_read_node[name=id]') 148 | expect(expression.query_nodes(node)).to eq [node.body.first.body.body.first.body.body.first.value] 149 | end 150 | 151 | it 'matches next sibling node' do 152 | expression = parser.parse('.instance_variable_write_node[name=@id] + .instance_variable_write_node[name=@name]') 153 | expect(expression.query_nodes(node)).to eq [node.body.first.body.body.first.body.body.last] 154 | end 155 | 156 | it 'matches sebsequent sibling node' do 157 | expression = parser.parse('.instance_variable_write_node[name=@id] ~ .instance_variable_write_node[name=@name]') 158 | expect(expression.query_nodes(node)).to eq [node.body.first.body.body.first.body.body.last] 159 | end 160 | 161 | it 'matches goto scope' do 162 | expression = parser.parse('.def_node body .instance_variable_write_node[name=@id]') 163 | expect(expression.query_nodes(node)).to eq [node.body.first.body.body.first.body.body.first] 164 | end 165 | 166 | it 'matches multiple goto scope' do 167 | expression = parser.parse('.def_node body.body .instance_variable_write_node[name=@id]') 168 | expect(expression.query_nodes(node)).to eq [node.body.first.body.body.first.body.body.first] 169 | end 170 | 171 | it 'matches has selector' do 172 | expression = parser.parse('.def_node:has(.instance_variable_write_node[name=@id])') 173 | expect(expression.query_nodes(node)).to eq [node.body.first.body.body.first] 174 | end 175 | 176 | it 'matches not_has selector' do 177 | expression = parser.parse('.def_node:not_has(.instance_variable_write_node[name=@id])') 178 | expect(expression.query_nodes(node)).to eq [] 179 | end 180 | 181 | it 'matches root has selector' do 182 | expression = parser.parse(':has(> .class_node)') 183 | expect(expression.query_nodes(node)).to eq [node] 184 | end 185 | 186 | it 'matches root not_has selector' do 187 | expression = parser.parse(':not_has(> .class_node)') 188 | expect(expression.query_nodes(node)).to eq [] 189 | end 190 | 191 | it 'matches >=' do 192 | expression = parser.parse('.def_node[parameters.requireds.size>=2]') 193 | expect(expression.query_nodes(node)).to eq [node.body.first.body.body.first] 194 | end 195 | 196 | it 'matches >' do 197 | expression = parser.parse('.def_node[parameters.requireds.size>2]') 198 | expect(expression.query_nodes(node)).to eq [] 199 | end 200 | 201 | it 'matches <=' do 202 | expression = parser.parse('.def_node[parameters.requireds.size<=2]') 203 | expect(expression.query_nodes(node)).to eq [node.body.first.body.body.first] 204 | end 205 | 206 | it 'matches <' do 207 | expression = parser.parse('.def_node[parameters.requireds.size<2]') 208 | expect(expression.query_nodes(node)).to eq [] 209 | end 210 | 211 | it 'matches arguments' do 212 | expression = parser.parse('.call_node[arguments.arguments.size=2][arguments.arguments.first=.integer_node][arguments.arguments.last=.string_node]') 213 | expect(expression.query_nodes(node)).to eq [node.body.last.value] 214 | 215 | expression = parser.parse('.call_node[arguments.arguments.size=2][arguments.arguments.0=.integer_node][arguments.arguments.-1=.string_node]') 216 | expect(expression.query_nodes(node)).to eq [node.body.last.value] 217 | end 218 | 219 | it 'matches evaluated value' do 220 | expression = parser.parse('.instance_variable_write_node[name="@{{value}}"]') 221 | expect(expression.query_nodes(node)).to eq node.body.first.body.body.first.body.body 222 | end 223 | 224 | it 'matches evaluated value from base node' do 225 | expression = parser.parse('.def_node[name=initialize][body.body.0.name="@{{body.body.0.value}}"]') 226 | expect(expression.query_nodes(node)).to eq [node.body.first.body.body.first] 227 | end 228 | 229 | it 'matches ,' do 230 | expression = parser.parse('.instance_variable_write_node[name=@id], .instance_variable_write_node[name=@name]') 231 | expect(expression.query_nodes(node)).to eq [ 232 | node.body.first.body.body.first.body.body.first, 233 | node.body.first.body.body.first.body.body.last 234 | ] 235 | end 236 | 237 | it 'matches :first-child' do 238 | expression = parser.parse('.instance_variable_write_node:first-child') 239 | expect(expression.query_nodes(node)).to eq [node.body.first.body.body.first.body.body.first] 240 | 241 | expression = parser.parse('.def_node .instance_variable_write_node:first-child') 242 | expect(expression.query_nodes(node)).to eq [node.body.first.body.body.first.body.body.first] 243 | 244 | expression = parser.parse('.CommandCall:first-child') 245 | expect(expression.query_nodes(node)).to eq [] 246 | end 247 | 248 | it 'matches :last-child' do 249 | expression = parser.parse('.def_node .instance_variable_write_node:last-child') 250 | expect(expression.query_nodes(node)).to eq [node.body.first.body.body.first.body.body.last] 251 | 252 | expression = parser.parse('.CommandCall:last-child') 253 | expect(expression.query_nodes(node)).to eq [] 254 | end 255 | 256 | it 'matches empty string' do 257 | node = prism_parse("call('')") 258 | expression = parser.parse(".call_node[message=call][arguments.arguments.first='']") 259 | expect(expression.query_nodes(node)).to eq [node.body.first] 260 | end 261 | 262 | it 'matches hash value' do 263 | node = prism_parse("{ foo: 'bar' }") 264 | expression = parser.parse(".hash_node[foo_value='bar']") 265 | expect(expression.query_nodes(node)).to eq [node.body.first] 266 | end 267 | end 268 | end 269 | end 270 | -------------------------------------------------------------------------------- /spec/node_query_parser/syntax_tree_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'oedipus_lex' 3 | 4 | RSpec.describe NodeQueryParser do 5 | let(:parser) { described_class.new(adapter: NodeQuery::SyntaxTreeAdapter.new) } 6 | 7 | describe '#query_nodes' do 8 | context 'syntax_tree' do 9 | let(:node) { 10 | syntax_tree_parse(<<~EOS) 11 | class User < Base 12 | def initialize(id, name) 13 | @id = id 14 | @name = name 15 | end 16 | end 17 | 18 | user = User.new(1, "Murphy") 19 | EOS 20 | } 21 | 22 | it 'matches node type' do 23 | expression = parser.parse('.DefNode') 24 | expect(expression.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 25 | end 26 | 27 | it 'matches node type and one attribute' do 28 | expression = parser.parse('.ClassDeclaration[constant=User]') 29 | expect(expression.query_nodes(node)).to eq [node.body.first] 30 | end 31 | 32 | it 'matches nested attribute' do 33 | expression = parser.parse('.ClassDeclaration[superclass.value=Base]') 34 | expect(expression.query_nodes(node)).to eq [node.body.first] 35 | end 36 | 37 | it 'matches method result' do 38 | expression = parser.parse('.DefNode[params.contents.requireds.size=2]') 39 | expect(expression.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 40 | end 41 | 42 | it 'matches multiple attributes' do 43 | expression = parser.parse('.DefNode[params.contents.requireds.size=2][params.contents.requireds.0=id][params.contents.requireds.1=name]') 44 | expect(expression.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 45 | end 46 | 47 | it 'matches ^=' do 48 | expression = parser.parse('.DefNode[name^=init]') 49 | expect(expression.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 50 | end 51 | 52 | it 'matches $=' do 53 | expression = parser.parse('.DefNode[name$=ize]') 54 | expect(expression.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 55 | end 56 | 57 | it 'matches *=' do 58 | expression = parser.parse('.DefNode[name*=ial]') 59 | expect(expression.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 60 | end 61 | 62 | it 'matches !=' do 63 | expression = parser.parse('.DefNode[name!=foobar]') 64 | expect(expression.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 65 | end 66 | 67 | it 'matches =~' do 68 | expression = parser.parse('.DefNode[name=~/init/]') 69 | expect(expression.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 70 | end 71 | 72 | it 'matches !~' do 73 | expression = parser.parse('.DefNode[name!~/bar/]') 74 | expect(expression.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 75 | end 76 | 77 | it 'matches in' do 78 | expression = parser.parse('.IVar[value IN (@id @name)]') 79 | expect(expression.query_nodes(node)).to eq [ 80 | node.body.first.bodystmt.statements.body.first.bodystmt.statements.body.first.target.value, 81 | node.body.first.bodystmt.statements.body.first.bodystmt.statements.body.last.target.value 82 | ] 83 | end 84 | 85 | it 'matches not in' do 86 | expression = parser.parse('.IVar[value NOT IN (@id @name)]') 87 | expect(expression.query_nodes(node)).to eq [] 88 | end 89 | 90 | it 'matches includes' do 91 | expression = parser.parse('.DefNode[params.contents.requireds INCLUDES id]') 92 | expect(expression.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 93 | end 94 | 95 | it 'matches includes with selector' do 96 | expression = parser.parse('.DefNode[params.contents.requireds INCLUDES .Ident[value=id]]') 97 | expect(expression.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 98 | end 99 | 100 | it 'matches not includes' do 101 | expression = parser.parse('.DefNode[params.contents.requireds NOT INCLUDES foobar]') 102 | expect(expression.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 103 | end 104 | 105 | it 'matches not includes with selector' do 106 | expression = parser.parse('.DefNode[params.contents.requireds NOT INCLUDES .Ident[value=foobar]]') 107 | expect(expression.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 108 | end 109 | 110 | it 'matches equal array' do 111 | expression = parser.parse('.DefNode[params.contents.requireds=(id name)]') 112 | expect(expression.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 113 | 114 | expression = parser.parse('.DefNode[params.contents.requireds=(name id)]') 115 | expect(expression.query_nodes(node)).to eq [] 116 | end 117 | 118 | it 'matches not equal array' do 119 | expression = parser.parse('.DefNode[params.contents.requireds!=(id name)]') 120 | expect(expression.query_nodes(node)).to eq [] 121 | 122 | expression = parser.parse('.DefNode[params.contents.requireds!=(name id)]') 123 | expect(expression.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 124 | end 125 | 126 | it 'matches nested selector' do 127 | expression = parser.parse('.DefNode[bodystmt.statements.body.0=.Assign]') 128 | expect(expression.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 129 | end 130 | 131 | it 'matches * in attribute key' do 132 | expression = parser.parse('.DefNode[params.contents.requireds.*.value=(id name)]') 133 | expect(expression.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 134 | end 135 | 136 | it 'matches descendant node' do 137 | expression = parser.parse('.ClassDeclaration .IVar[value=@id]') 138 | expect(expression.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first.bodystmt.statements.body.first.target.value] 139 | end 140 | 141 | it 'matches three level descendant node' do 142 | expression = parser.parse('.ClassDeclaration .DefNode .IVar[value=@id]') 143 | expect(expression.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first.bodystmt.statements.body.first.target.value] 144 | end 145 | 146 | it 'matches child node' do 147 | expression = parser.parse('.Assign > .VarField[value=@id]') 148 | expect(expression.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first.bodystmt.statements.body.first.target] 149 | end 150 | 151 | it 'matches next sibling node' do 152 | expression = parser.parse('.Assign[target=@id] + .Assign[target=@name]') 153 | expect(expression.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first.bodystmt.statements.body.last] 154 | end 155 | 156 | it 'matches sebsequent sibling node' do 157 | expression = parser.parse('.Assign[target=@id] ~ .Assign[target=@name]') 158 | expect(expression.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first.bodystmt.statements.body.last] 159 | end 160 | 161 | it 'matches goto scope' do 162 | expression = parser.parse('.DefNode bodystmt .IVar[value=@id]') 163 | expect(expression.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first.bodystmt.statements.body.first.target.value] 164 | end 165 | 166 | it 'matches multiple goto scope' do 167 | expression = parser.parse('.DefNode bodystmt.statements.body .IVar[value=@id]') 168 | expect(expression.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first.bodystmt.statements.body.first.target.value] 169 | end 170 | 171 | it 'matches has selector' do 172 | expression = parser.parse('.DefNode:has(.IVar[value=@id])') 173 | expect(expression.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 174 | end 175 | 176 | it 'matches not_has selector' do 177 | expression = parser.parse('.DefNode:not_has(.IVar[value=@id])') 178 | expect(expression.query_nodes(node)).to eq [] 179 | end 180 | 181 | it 'matches root has selector' do 182 | expression = parser.parse(':has(> .ClassDeclaration)') 183 | expect(expression.query_nodes(node)).to eq [node] 184 | end 185 | 186 | it 'matches root not_has selector' do 187 | expression = parser.parse(':not_has(> .ClassDeclaration)') 188 | expect(expression.query_nodes(node)).to eq [] 189 | end 190 | 191 | it 'matches >=' do 192 | expression = parser.parse('.DefNode[params.contents.requireds.size>=2]') 193 | expect(expression.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 194 | end 195 | 196 | it 'matches >' do 197 | expression = parser.parse('.DefNode[params.contents.requireds.size>2]') 198 | expect(expression.query_nodes(node)).to eq [] 199 | end 200 | 201 | it 'matches <=' do 202 | expression = parser.parse('.DefNode[params.contents.requireds.size<=2]') 203 | expect(expression.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 204 | end 205 | 206 | it 'matches <' do 207 | expression = parser.parse('.DefNode[params.contents.requireds.size<2]') 208 | expect(expression.query_nodes(node)).to eq [] 209 | end 210 | 211 | it 'matches arguments' do 212 | expression = parser.parse('.CallNode[arguments.arguments.parts.size=2][arguments.arguments.parts.first=.Int][arguments.arguments.parts.last=.StringLiteral]') 213 | expect(expression.query_nodes(node)).to eq [node.body.last.value] 214 | 215 | expression = parser.parse('.CallNode[arguments.arguments.parts.size=2][arguments.arguments.parts.0=.Int][arguments.arguments.parts.-1=.StringLiteral]') 216 | expect(expression.query_nodes(node)).to eq [node.body.last.value] 217 | end 218 | 219 | it 'matches evaluated value' do 220 | expression = parser.parse('.Assign[target="@{{value}}"]') 221 | expect(expression.query_nodes(node)).to eq node.body.first.bodystmt.statements.body.first.bodystmt.statements.body 222 | end 223 | 224 | it 'matches evaluated value from base node' do 225 | expression = parser.parse('.DefNode[name=initialize][bodystmt.statements.body.0.target="@{{bodystmt.statements.body.0.value}}"]') 226 | expect(expression.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first] 227 | end 228 | 229 | it 'matches ,' do 230 | expression = parser.parse('.IVar[value=@id], .IVar[value=@name]') 231 | expect(expression.query_nodes(node)).to eq [ 232 | node.body.first.bodystmt.statements.body.first.bodystmt.statements.body.first.target.value, 233 | node.body.first.bodystmt.statements.body.first.bodystmt.statements.body.last.target.value 234 | ] 235 | end 236 | 237 | it 'matches :first-child' do 238 | expression = parser.parse('.Assign:first-child') 239 | expect(expression.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first.bodystmt.statements.body.first] 240 | 241 | expression = parser.parse('.DefNode .Assign:first-child') 242 | expect(expression.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first.bodystmt.statements.body.first] 243 | 244 | expression = parser.parse('.CommandCall:first-child') 245 | expect(expression.query_nodes(node)).to eq [] 246 | end 247 | 248 | it 'matches :last-child' do 249 | expression = parser.parse('.DefNode .Assign:last-child') 250 | expect(expression.query_nodes(node)).to eq [node.body.first.bodystmt.statements.body.first.bodystmt.statements.body.last] 251 | 252 | expression = parser.parse('.CommandCall:last-child') 253 | expect(expression.query_nodes(node)).to eq [] 254 | end 255 | 256 | it 'matches empty string' do 257 | node = syntax_tree_parse("call('')") 258 | expression = parser.parse(".CallNode[message=call][arguments.arguments.parts.first='']") 259 | expect(expression.query_nodes(node)).to eq [node.body.first] 260 | end 261 | 262 | it 'matches hash value' do 263 | node = syntax_tree_parse("{ foo: 'bar' }") 264 | expression = parser.parse(".HashLiteral[foo_value='bar']") 265 | expect(expression.query_nodes(node)).to eq [node.body.first] 266 | end 267 | end 268 | end 269 | end 270 | -------------------------------------------------------------------------------- /spec/node_query_parser_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'oedipus_lex' 3 | 4 | RSpec.describe NodeQueryParser do 5 | let(:parser) { described_class.new(adapter: :parser) } 6 | 7 | describe '#toString' do 8 | it 'parses node ype' do 9 | source = '.send' 10 | expect(parser.parse(source).to_s).to eq source 11 | end 12 | 13 | it 'parses one selector' do 14 | source = '.send[message=:create]' 15 | expect(parser.parse(source).to_s).to eq source 16 | end 17 | 18 | it 'parses two selectors' do 19 | source = '.class[name=Synvert] .def[name="foobar"]' 20 | expect(parser.parse(source).to_s).to eq source 21 | end 22 | 23 | it 'parses three selectors' do 24 | source = '.class[name=Synvert] .def[name="foobar"] .send[message=create]' 25 | expect(parser.parse(source).to_s).to eq source 26 | end 27 | 28 | it 'parses child selector' do 29 | source = '.class[name=Synvert] > .def[name="foobar"]' 30 | expect(parser.parse(source).to_s).to eq source 31 | end 32 | 33 | it 'parses :has pseduo class selector' do 34 | source = '.class :has(> .def)' 35 | expect(parser.parse(source).to_s).to eq source 36 | end 37 | 38 | it 'parses :not_has pseduo class selector' do 39 | source = '.class :not_has(> .def)' 40 | expect(parser.parse(source).to_s).to eq source 41 | end 42 | 43 | it 'parses multiple attributes' do 44 | source = '.send[receiver=nil][message=:create]' 45 | expect(parser.parse(source).to_s).to eq source 46 | end 47 | 48 | it 'parses nested selector' do 49 | source = '.def[body.0=.send[message=create]]' 50 | expect(parser.parse(source).to_s).to eq source 51 | end 52 | 53 | it 'parses selector value' do 54 | source = '.send[receiver=.send[message=:create]]' 55 | expect(parser.parse(source).to_s).to eq source 56 | end 57 | 58 | it 'parses ^= operator' do 59 | source = '.def[name^=synvert]' 60 | expect(parser.parse(source).to_s).to eq source 61 | end 62 | 63 | it 'parses $= operator' do 64 | source = '.def[name$=synvert]' 65 | expect(parser.parse(source).to_s).to eq source 66 | end 67 | 68 | it 'parses *= operator' do 69 | source = '.def[name*=synvert]' 70 | expect(parser.parse(source).to_s).to eq source 71 | end 72 | 73 | it 'parses != operator' do 74 | source = '.send[receiver=.send[message!=:create]]' 75 | expect(parser.parse(source).to_s).to eq source 76 | end 77 | 78 | it 'parses > operator' do 79 | source = '.send[receiver=.send[arguments.size>1]]' 80 | expect(parser.parse(source).to_s).to eq source 81 | end 82 | 83 | it 'parses >= operator' do 84 | source = '.send[receiver=.send[arguments.size>=1]]' 85 | expect(parser.parse(source).to_s).to eq source 86 | end 87 | 88 | it 'parses < operator' do 89 | source = '.send[receiver=.send[arguments.size<1]]' 90 | expect(parser.parse(source).to_s).to eq source 91 | end 92 | 93 | it 'parses <= operator' do 94 | source = '.send[receiver=.send[arguments.size<=1]]' 95 | expect(parser.parse(source).to_s).to eq source 96 | end 97 | 98 | it 'parses in operator' do 99 | source = '.def[name in (foo bar)]' 100 | expect(parser.parse(source).to_s).to eq source 101 | end 102 | 103 | it 'parses not in operator' do 104 | source = '.def[name not in (foo bar)]' 105 | expect(parser.parse(source).to_s).to eq source 106 | end 107 | 108 | it 'parses includes operator' do 109 | source = '.def[arguments includes &block]' 110 | expect(parser.parse(source).to_s).to eq source 111 | end 112 | 113 | it 'parses not includes operator' do 114 | source = '.def[arguments not includes &block]' 115 | expect(parser.parse(source).to_s).to eq source 116 | end 117 | 118 | it 'parses empty string' do 119 | source = '.send[arguments.first=""]' 120 | expect(parser.parse(source).to_s).to eq source 121 | end 122 | 123 | it 'parses []=' do 124 | source = '.send[message=[]=]' 125 | expect(parser.parse(source).to_s).to eq source 126 | end 127 | 128 | it 'parses :[]' do 129 | source = '.send[message=:[]]' 130 | expect(parser.parse(source).to_s).to eq source 131 | end 132 | 133 | it 'parses goto scope' do 134 | source = '.block body > .send' 135 | expect(parser.parse(source).to_s).to eq source 136 | end 137 | 138 | it 'parses * in key' do 139 | source = '.def[arguments.*.name in (foo bar)]' 140 | expect(parser.parse(source).to_s).to eq source 141 | end 142 | 143 | it 'parses ,' do 144 | source = '.send[message=foo], .send[message=bar]' 145 | expect(parser.parse(source).to_s).to eq source 146 | end 147 | 148 | it 'parses :first-child' do 149 | source = '.send[message=foo]:first-child' 150 | expect(parser.parse(source).to_s).to eq source 151 | end 152 | 153 | it 'parses :last-child' do 154 | source = '.send[message=foo]:last-child' 155 | expect(parser.parse(source).to_s).to eq source 156 | end 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /spec/node_query_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe NodeQuery do 4 | let(:node) { 5 | parser_parse(<<~EOS) 6 | class Synvert 7 | def foo 8 | FactoryBot.create(:user, name: 'foo') 9 | end 10 | 11 | def bar 12 | FactoryBot.create(:user, name: 'bar') 13 | end 14 | 15 | def foobar(a, b) 16 | { a: a, b: b } 17 | arr[index] 18 | arr[index] = value 19 | nil? 20 | call('') 21 | end 22 | end 23 | EOS 24 | } 25 | 26 | it 'initializes with invalid adapter' do 27 | expect { 28 | described_class.new('.send', adapter: :invalid) 29 | }.to raise_error(NodeQuery::InvalidAdapterError) 30 | end 31 | 32 | describe "#query_nodes" do 33 | it "matches nql" do 34 | query = described_class.new('.class[name=Synvert]', adapter: :parser) 35 | expect(query.query_nodes(node)).to eq [node] 36 | end 37 | 38 | it "matches rules" do 39 | query = described_class.new({ node_type: 'class', name: 'Synvert' }, adapter: :parser) 40 | expect(query.query_nodes(node)).to eq [node] 41 | end 42 | end 43 | 44 | describe "#match_node?" do 45 | it "matches nql" do 46 | query = described_class.new('.class[name=Synvert]', adapter: :parser) 47 | expect(query.match_node?(node)).to be_truthy 48 | end 49 | 50 | it "matches rules" do 51 | query = described_class.new({ node_type: 'class', name: 'Synvert' }, adapter: :parser) 52 | expect(query.match_node?(node)).to be_truthy 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "node_query" 4 | 5 | Dir[File.join(File.dirname(__FILE__), 'support', '*')].each do |path| 6 | require path 7 | end 8 | 9 | RSpec.configure do |config| 10 | config.include ParserHelper 11 | 12 | # Enable flags like --only-failures and --next-failure 13 | config.example_status_persistence_file_path = ".rspec_status" 14 | 15 | # Disable RSpec exposing methods globally on `Module` and `main` 16 | config.disable_monkey_patching! 17 | 18 | config.expect_with :rspec do |c| 19 | c.syntax = :expect 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/support/parser_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'parser/current' 4 | require 'parser_node_ext' 5 | require 'syntax_tree_ext' 6 | require 'prism_ext' 7 | 8 | module ParserHelper 9 | def parser_parse(code) 10 | Parser::CurrentRuby.parse(code) 11 | end 12 | 13 | def syntax_tree_parse(code) 14 | SyntaxTree.parse(code).statements 15 | end 16 | 17 | def prism_parse(code) 18 | Prism.parse(code).value.statements 19 | end 20 | end 21 | --------------------------------------------------------------------------------