NodeQuery
NodeQuery defines a NQL (node query language) and node rules to query AST nodes.
Table of Contents
- NodeQuery
- Table of Contents
- Installation
- Usage
- Node Query Language
- nql matches node type
- nql matches attribute
- nql matches nested attribute
- nql matches evaluated value
- nql matches nested selector
- nql matches method result
- nql matches operators
- nql matches array node attribute
- nql matches * in attribute key
- nql matches multiple selectors
- Descendant combinator
- Child combinator
- Adjacent sibling combinator
- General sibling combinator
- nql matches goto scope
- nql matches :has and :not_has pseudo selector
- nql matches :first-child and :last-child pseudo selector
- nql matches multiple expressions
- Node Rules
- rules matches node type
- rules matches attribute
- rules matches nested attribute
- rules matches evaluated value
- rules matches nested selector
- rules matches method result
- rules matches operators
- rules matches array nodes attribute
- Write Adapter
- Development
- Contributing
Installation
Add this line to your application's Gemfile:
gem 'node_query'
And then execute:
$ bundle install
Or install it yourself as:
$ gem install node_query
Usage
It provides two apis: query_nodes
and match_node?
node_query = NodeQuery.new(nql_or_rules: String | Hash, adapter: Symbol) # Initialize NodeQuery
node_query.query_nodes(node: Node, options = { including_self: true, stop_at_first_match: false, recursive: true }): Node[] # Get the matching nodes.
node_query.match_node?(node: Node): boolean # Check if the node matches nql or rules.
Here is an example for parser ast node.
source = `
class User
def initialize(id, name)
@id = id
@name = name
end
end
user = User.new(1, "Murphy")
`
node = Parser::CurrentRuby.parse(source)
# It will get the node of initialize.
NodeQuery.new('.def[name=initialize]', adapter: :parser).query_nodes(node)
NodeQuery.new({ node_type: 'def', name: 'initialize' }, adapter: :parser).query_nodes(node)
Node Query Language
nql matches node type
.class
It matches class node
nql matches attribute
.class[name=User]
It matches class node whose name is User
nql matches nested attribute
.class[parent_class.name=Base]
It matches class node whose parent class name is Base
nql matches evaluated value
.ivasgn[variable="@{{value}}"]
It matches ivasgn node whose left value equals '@' plus the evaluated value of right value.
nql matches nested selector
.def[body.0=.ivasgn]
It matches def node whose first child node is an ivasgn node.
nql matches method result
.def[arguments.size=2]
It matches def node whose arguments size is 2.
nql matches operators
.class[name=User]
Value of name is equal to User
.class[name^=User]
Value of name starts with User
.class[name$=User]
Value of name ends with User
.class[name*=User]
Value of name contains User
.def[arguments.size!=2]
Size of arguments is not equal to 2
.def[arguments.size>=2]
Size of arguments is greater than or equal to 2
.def[arguments.size>2]
Size of arguments is greater than 2
.def[arguments.size<=2]
Size of arguments is less than or equal to 2
.def[arguments.size<2]
Size of arguments is less than 2
.class[name IN (User Account)]
Value of name is either User or Account
.class[name NOT IN (User Account)]
Value of name is neither User nor Account
.def[arguments INCLUDES id]
Value of arguments includes id
.def[arguments NOT INCLUDES id]
Value of arguments not includes id
.class[name=~/User/]
Value of name matches User
.class[name!~/User/]
Value of name does not match User
.class[name IN (/User/ /Account/)]
Value of name matches either /User/ or /Account/
nql matches array node attribute
.def[arguments=(id name)]
It matches def node whose arguments are id and name.
nql matches * in attribute key
.def[arguments.*.name IN (id name)]
It matches def node whose arguments are either id or name.
nql matches multiple selectors
Descendant combinator
.class .send
It matches send node whose ancestor is class node.
Child combinator
.def > .send
It matches send node whose parent is def node.
Adjacent sibling combinator
.send[variable=@id] + .send
It matches send node only if it is immediately follows the send node whose left value is @id.
General sibling combinator
.send[variable=@id] ~ .send
It matches send node only if it is follows the send node whose left value is @id.
nql matches goto scope
.def body .send
It matches send node who is in the body of def node.
nql matches :has and :not_has pseudo selector
.class:has(.def[name=initialize])
It matches class node who has an initialize def node.
.class:not_has(.def[name=initialize])
It matches class node who does not have an initialize def node.
nql matches :first-child and :last-child pseudo selector
.def:first-child
It matches the first def node.
.def:last-child
It matches the last def node.
nql matches multiple expressions
.ivasgn[variable=@id], .ivasgn[variable=@name]
It matches ivasgn node whose left value is either @id or @name.
Node Rules
rules matches node type
{ node_type: 'class' }
It matches class node
rules matches attribute
{ node_type: 'def', name: 'initialize' }
It matches def node whose name is initialize
{ node_type: 'def', arguments: { "0": 1, "1": "Murphy" } }
It matches def node whose arguments are 1 and Murphy.
rules matches nested attribute
{ node_type: 'class', parent_class: { name: 'Base' } }
It matches class node whose parent class name is Base
rules matches evaluated value
{ node_type: 'ivasgn', variable: '@{{value}}' }
It matches ivasgn node whose left value equals '@' plus the evaluated value of right value.
rules matches nested selector
{ node_type: 'def', body: { "0": { node_type: 'ivasgn' } } }
It matches def node whose first child node is an ivasgn node.
rules matches method result
{ node_type: 'def', arguments: { size: 2 } }
It matches def node whose arguments size is 2.
rules matches operators
{ node_type: 'class', name: 'User' }
Value of name is equal to User
{ node_type: 'def', arguments: { size { not: 2 } }
Size of arguments is not equal to 2
{ node_type: 'def', arguments: { size { gte: 2 } }
Size of arguments is greater than or equal to 2
{ node_type: 'def', arguments: { size { gt: 2 } }
Size of arguments is greater than 2
{ node_type: 'def', arguments: { size { lte: 2 } }
Size of arguments is less than or equal to 2
{ node_type: 'def', arguments: { size { lt: 2 } }
Size of arguments is less than 2
{ node_type: 'class', name: { in: ['User', 'Account'] } }
Value of name is either User or Account
{ node_type: 'class', name: { not_in: ['User', 'Account'] } }
Value of name is neither User nor Account
{ node_type: 'def', arguments: { includes: 'id' } }
Value of arguments includes id
{ node_type: 'def', arguments: { not_includes: 'id' } }
Value of arguments not includes id
{ node_type: 'class', name: /User/ }
Value of name matches User
{ node_type: 'class', name: { not: /User/ } }
Value of name does not match User
{ node_type: 'class', name: { in: [/User/, /Account/] } }
Value of name matches either /User/ or /Account/
rules matches array nodes attribute
{ node_type: 'def', arguments: ['id', 'name'] }
It matches def node whose arguments are id and name.
Write Adapter
Different parser, like prism, parser, syntax_tree, will generate different AST nodes, to make NodeQuery work for them all, we define an Adapter interface, if you implement the Adapter interface, you can set it as NodeQuery's adapter.
It provides 3 adapters
PrismAdapter
ParserAdapter
SyntaxTreeAdapter
Development
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.
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.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/synvert-hq/node-query-ruby.