tree_stand 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 736381046f8ec987fd106632c34cfbcf79055801d19c519d59980da52d9ebf6a
4
+ data.tar.gz: 7e1563ffb2af294542d3ac35fd287738bae0ebb69f7312907c18b5ff07fafd64
5
+ SHA512:
6
+ metadata.gz: 36c90d3233c617abb88bb364cd79bc8fc0465301ef91dd0f9043cbc390d7e7ea5bf7f6ddf07ce4d82f715fd6dc2f76a3c3886a601610c29ddd6679effe2aa3f9
7
+ data.tar.gz: 3ffc48adac74e5623244e0024534c867f074715d03640f7a428b077556e71f5d5b9b9394ad6f02c586b76768878f13511f3772fa70a5871d9fedea537eca6f67
@@ -0,0 +1,44 @@
1
+ name: TreeStand
2
+ on:
3
+ push:
4
+ branches:
5
+ - "main"
6
+ pull_request:
7
+ types: [opened, synchronize, reopened, ready_for_review]
8
+ branches:
9
+ - 'main'
10
+ jobs:
11
+ test:
12
+ runs-on: ubuntu-latest
13
+ strategy:
14
+ matrix:
15
+ entry:
16
+ - { ruby: 3.0 }
17
+ - { ruby: 3.1 }
18
+ name: test (${{ matrix.entry.ruby }})
19
+ steps:
20
+ - uses: actions/checkout@v3
21
+
22
+ - uses: actions/checkout@v3
23
+ with:
24
+ repository: DerekStride/tree-sitter-sql
25
+ path: tmp/tree-sitter-sql
26
+
27
+ - uses: ruby/setup-ruby@v1
28
+ with:
29
+ ruby-version: ${{ matrix.entry.ruby }}
30
+ - uses: actions/cache@v1
31
+ with:
32
+ path: vendor/bundle
33
+ key: ${{ runner.os }}-gems-${{ hashFiles('Gemfile') }}
34
+ restore-keys: ${{ runner.os }}-gems-
35
+
36
+ - uses: actions/setup-node@v3
37
+ with:
38
+ node-version: 16
39
+ - run: npm install tree-sitter-cli
40
+
41
+ - run: sudo apt-get install -y libtree-sitter-dev make gcc
42
+ - run: bundle install --jobs=3 --retry=3 --path=vendor/bundle
43
+ - run: bin/setup
44
+ - run: bundle exec rake
@@ -0,0 +1,22 @@
1
+ name: Contributor License Agreement (CLA)
2
+
3
+ on:
4
+ pull_request_target:
5
+ types: [opened, synchronize]
6
+ issue_comment:
7
+ types: [created]
8
+
9
+ jobs:
10
+ cla:
11
+ runs-on: ubuntu-latest
12
+ if: |
13
+ (github.event.issue.pull_request
14
+ && !github.event.issue.pull_request.merged_at
15
+ && contains(github.event.comment.body, 'signed')
16
+ )
17
+ || (github.event.pull_request && !github.event.pull_request.merged)
18
+ steps:
19
+ - uses: Shopify/shopify-cla-action@v1
20
+ with:
21
+ github-token: ${{ secrets.GITHUB_TOKEN }}
22
+ cla-token: ${{ secrets.CLA_TOKEN }}
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /parsers/
10
+ Gemfile.lock
@@ -0,0 +1 @@
1
+ v1
@@ -0,0 +1,23 @@
1
+ containers:
2
+ default:
3
+ build:
4
+ from: ubuntu-latest
5
+ type: ci
6
+ ruby: 3.1
7
+ rust: stable
8
+
9
+ steps:
10
+ - label: Publish Gem
11
+ timeout: 30m
12
+ run:
13
+ - cargo install tree-sitter-cli
14
+ - tree-sitter -V
15
+ - mkdir -p tmp
16
+ - cd tmp
17
+ - git clone --depth=1 https://github.com/tree-sitter/tree-sitter.git
18
+ - cd tree-sitter
19
+ - make
20
+ - make install
21
+ - cd ../..
22
+ - publish:
23
+ type: gem
@@ -0,0 +1,134 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ Contact: opensource@shopify.com
4
+
5
+ ## Our Pledge
6
+
7
+ We as members, contributors, and leaders pledge to make participation in our
8
+ community a harassment-free experience for everyone, regardless of age, body
9
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
10
+ identity and expression, level of experience, education, socio-economic status,
11
+ nationality, personal appearance, race, caste, color, religion, or sexual
12
+ identity and orientation.
13
+
14
+ We pledge to act and interact in ways that contribute to an open, welcoming,
15
+ diverse, inclusive, and healthy community.
16
+
17
+ ## Our Standards
18
+
19
+ Examples of behavior that contributes to a positive environment for our
20
+ community include:
21
+
22
+ * Demonstrating empathy and kindness toward other people
23
+ * Being respectful of differing opinions, viewpoints, and experiences
24
+ * Giving and gracefully accepting constructive feedback
25
+ * Accepting responsibility and apologizing to those affected by our mistakes,
26
+ and learning from the experience
27
+ * Focusing on what is best not just for us as individuals, but for the overall
28
+ community
29
+
30
+ Examples of unacceptable behavior include:
31
+
32
+ * The use of sexualized language or imagery, and sexual attention or advances of
33
+ any kind
34
+ * Trolling, insulting or derogatory comments, and personal or political attacks
35
+ * Public or private harassment
36
+ * Publishing others' private information, such as a physical or email address,
37
+ without their explicit permission
38
+ * Other conduct which could reasonably be considered inappropriate in a
39
+ professional setting
40
+
41
+ ## Enforcement Responsibilities
42
+
43
+ Community leaders are responsible for clarifying and enforcing our standards of
44
+ acceptable behavior and will take appropriate and fair corrective action in
45
+ response to any behavior that they deem inappropriate, threatening, offensive,
46
+ or harmful.
47
+
48
+ Community leaders have the right and responsibility to remove, edit, or reject
49
+ comments, commits, code, wiki edits, issues, and other contributions that are
50
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
51
+ decisions when appropriate.
52
+
53
+ ## Scope
54
+
55
+ This Code of Conduct applies within all community spaces, and also applies when
56
+ an individual is officially representing the community in public spaces.
57
+ Examples of representing our community include using an official e-mail address,
58
+ posting via an official social media account, or acting as an appointed
59
+ representative at an online or offline event.
60
+
61
+ ## Enforcement
62
+
63
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
64
+ reported to the community leaders responsible for enforcement at
65
+ [INSERT CONTACT METHOD].
66
+ All complaints will be reviewed and investigated promptly and fairly.
67
+
68
+ All community leaders are obligated to respect the privacy and security of the
69
+ reporter of any incident.
70
+
71
+ ## Enforcement Guidelines
72
+
73
+ Community leaders will follow these Community Impact Guidelines in determining
74
+ the consequences for any action they deem in violation of this Code of Conduct:
75
+
76
+ ### 1. Correction
77
+
78
+ **Community Impact**: Use of inappropriate language or other behavior deemed
79
+ unprofessional or unwelcome in the community.
80
+
81
+ **Consequence**: A private, written warning from community leaders, providing
82
+ clarity around the nature of the violation and an explanation of why the
83
+ behavior was inappropriate. A public apology may be requested.
84
+
85
+ ### 2. Warning
86
+
87
+ **Community Impact**: A violation through a single incident or series of
88
+ actions.
89
+
90
+ **Consequence**: A warning with consequences for continued behavior. No
91
+ interaction with the people involved, including unsolicited interaction with
92
+ those enforcing the Code of Conduct, for a specified period of time. This
93
+ includes avoiding interactions in community spaces as well as external channels
94
+ like social media. Violating these terms may lead to a temporary or permanent
95
+ ban.
96
+
97
+ ### 3. Temporary Ban
98
+
99
+ **Community Impact**: A serious violation of community standards, including
100
+ sustained inappropriate behavior.
101
+
102
+ **Consequence**: A temporary ban from any sort of interaction or public
103
+ communication with the community for a specified period of time. No public or
104
+ private interaction with the people involved, including unsolicited interaction
105
+ with those enforcing the Code of Conduct, is allowed during this period.
106
+ Violating these terms may lead to a permanent ban.
107
+
108
+ ### 4. Permanent Ban
109
+
110
+ **Community Impact**: Demonstrating a pattern of violation of community
111
+ standards, including sustained inappropriate behavior, harassment of an
112
+ individual, or aggression toward or disparagement of classes of individuals.
113
+
114
+ **Consequence**: A permanent ban from any sort of public interaction within the
115
+ community.
116
+
117
+ ## Attribution
118
+
119
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
120
+ version 2.1, available at
121
+ [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
122
+
123
+ Community Impact Guidelines were inspired by
124
+ [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
125
+
126
+ For answers to common questions about this code of conduct, see the FAQ at
127
+ [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
128
+ [https://www.contributor-covenant.org/translations][translations].
129
+
130
+ [homepage]: https://www.contributor-covenant.org
131
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
132
+ [Mozilla CoC]: https://github.com/mozilla/diversity
133
+ [FAQ]: https://www.contributor-covenant.org/faq
134
+ [translations]: https://www.contributor-covenant.org/translations
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ gem 'tree_sitter', git: 'https://github.com/Faveod/ruby-tree-sitter'
6
+
7
+ group :development do
8
+ gem "yard"
9
+ end
10
+
11
+ group :test do
12
+ gem "minitest-focus"
13
+ gem "minitest-reporters"
14
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 derekstride
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # TreeStand
2
+
3
+ [![TreeStand](https://github.com/Shopify/tree_stand/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Shopify/tree_stand/actions/workflows/ci.yml)
4
+
5
+
6
+ TreeStand is a high-level Ruby wrapper for the [Tree-sitter](https://tree-sitter.github.io/tree-sitter/) bindings. It
7
+ makes it easier to configure the parsers, and work with the underlying syntax tree.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem "tree_stand"
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ### Setting Up a Parser
20
+
21
+ TreeStand do not help with compiling individual parsers. However, once you compile a parser and generate a shared
22
+ object (`.so`) or a dynamic library (`.dylib`) you can tell TreeStand where to find them and pass the parser filename
23
+ to `TreeStand::Parser::new`.
24
+
25
+ ```ruby
26
+ TreeStand.configure do
27
+ config.parser_path = "path/to/parser/folder/"
28
+ end
29
+
30
+ sql_parser = TreeStand::Parser.new("sql")
31
+ ruby_parser = TreeStand::Parser.new("ruby")
32
+ ```
33
+
34
+
35
+ ### API Conventions
36
+
37
+ TreeStand aims to provide APIs similar to TreeSitter when possible. For example, the TreeSitter parser exposes a
38
+ `#parse_string(tree, document)` method. TreeStand replicates this behaviour but augments it to return a
39
+ `TreeStand::Tree` instead of the underlying `TreeSitter::Tree`. Similarly, `TreeStand::Tree#root_node` returns a
40
+ `TreeStand::Node` & `TreeSitter::Tree#root_node` returns a `TreeSitter::Node`.
41
+
42
+ The underlying objects are accessible via a `ts_` prefixed attribute, e.g. `ts_parser`, `ts_tree`, `ts_node`, etc.
43
+
44
+ ## Contributing
45
+
46
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/tree_stand. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
47
+
48
+ ## License
49
+
50
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
51
+
52
+ ## Code of Conduct
53
+
54
+ Everyone interacting in the TreeStand project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/Shopify/tree_stand/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << 'lib' << 'test'
6
+ t.pattern = 'test/**/*_test.rb'
7
+ end
8
+
9
+ task default: :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "tree_stand"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,20 @@
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
+ if [[ ! -d tmp/tree-sitter-sql ]]; then
10
+ mkdir -p tmp
11
+ git -C tmp/ clone --depth=1 https://github.com/DerekStride/tree-sitter-sql.git
12
+ fi
13
+
14
+ cd tmp/tree-sitter-sql
15
+
16
+ npm install
17
+ gcc -shared -o target/parser.so -fPIC src/parser.c -I./src
18
+
19
+ cd ../..
20
+ cp tmp/tree-sitter-sql/target/parser.so parsers/sql.so
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -ex
4
+
5
+ PREFIX="$(pwd)/tmp"
6
+
7
+ mkdir -p tmp/lib tmp/lib
8
+ cd tmp
9
+
10
+ git clone --depth=1 https://github.com/tree-sitter/tree-sitter.git 2> /dev/null
11
+ cd tree-sitter
12
+
13
+ make
14
+ PREFIX=$PREFIX make install
15
+ cd ../..
16
+
17
+ bundle config set build.tree_sitter \
18
+ --with-tree-sitter-dir=$PREFIX \
19
+ --with-tree-sitter-lib=$PREFIX/lib \
20
+ --with-tree-sitter-include=$PREFIX/include \
21
+ --with-opt-include=$PREFIX/include \
22
+ --with-opt-lib=$PREFIX/lib
@@ -0,0 +1,25 @@
1
+ module TreeStand
2
+ # An experimental class to modify the AST. I re-runs the query on the
3
+ # modified document every loop to ensure that the match is still valid.
4
+ # @see TreeStand::Tree
5
+ # @api experimental
6
+ class AstModifier
7
+ # @param tree [TreeStand::Tree]
8
+ def initialize(tree)
9
+ @tree = tree
10
+ end
11
+
12
+ # @param query [String]
13
+ # @yieldparam self [self]
14
+ # @yieldparam match [TreeStand::Match]
15
+ # @return [void]
16
+ def on_match(query)
17
+ matches = @tree.query(query)
18
+
19
+ while !matches.empty?
20
+ yield self, matches.first
21
+ matches = @tree.query(query)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,45 @@
1
+ module TreeStand
2
+ # Wrapper around a TreeSitter capture.
3
+ # @see TreeStand::Tree#query
4
+ # @see TreeStand::Match
5
+ class Capture
6
+ # @return [TreeStand::Match]
7
+ attr_reader :match
8
+ # @return [TreeSitter::Capture]
9
+ attr_reader :ts_capture
10
+
11
+ # @api private
12
+ def initialize(match, ts_capture)
13
+ @match = match
14
+ @ts_capture = ts_capture
15
+ end
16
+
17
+ # The name of the capture. TreeSitter strips the `@` from the capture name.
18
+ # @example
19
+ # match = @tree.query(<<~QUERY).first
20
+ # (identifier) @identifier.name
21
+ # QUERY
22
+ #
23
+ # capture = match.captures.first
24
+ #
25
+ # assert_equal("identifier.name", capture.name)
26
+ # @return [String]
27
+ def name
28
+ @match.ts_query.capture_name_for_id(@ts_capture.index)
29
+ end
30
+
31
+ # @return [TreeStand::Node]
32
+ def node
33
+ TreeStand::Node.new(@match.tree, @ts_capture.node)
34
+ end
35
+
36
+ # @param other [Object]
37
+ # @return [bool]
38
+ def ==(other)
39
+ return false unless other.is_a?(TreeStand::Capture)
40
+
41
+ name == other.name &&
42
+ node == other.node
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,8 @@
1
+ module TreeStand
2
+ # Global configuration for the gem.
3
+ # @api private
4
+ class Config
5
+ # @return [String]
6
+ attr_accessor :parser_path
7
+ end
8
+ end
@@ -0,0 +1,57 @@
1
+ module TreeStand
2
+ # Wrapper around a TreeSitter match.
3
+ # @see TreeStand::Tree#query
4
+ # @see TreeStand::Capture
5
+ class Match
6
+ # @return [TreeStand::Tree]
7
+ attr_reader :tree
8
+ # @return [TreeSitter::Query]
9
+ attr_reader :ts_query
10
+ # @return [TreeSitter::Match]
11
+ attr_reader :ts_match
12
+
13
+ # @api private
14
+ def initialize(tree, ts_query, ts_match)
15
+ @tree = tree
16
+ @ts_query = ts_query
17
+ @ts_match = ts_match
18
+
19
+ # TODO: This is a hack to get the captures to be populated.
20
+ # See: https://github.com/Faveod/ruby-tree-sitter/pull/16
21
+ captures
22
+ end
23
+
24
+ # Looks up a capture by name. TreeSitter strips the `@` from the capture name.
25
+ # @example
26
+ # match = @tree.query(<<~QUERY).first
27
+ # (identifier) @identifier.name
28
+ # QUERY
29
+ #
30
+ # refute_nil(match["identifier.name"])
31
+ # @param capture_name [String] The name of the capture from the query.
32
+ # @return [TreeStand::Capture, nil]
33
+ def [](capture_name)
34
+ captures.find { |capture| capture.name == capture_name }
35
+ end
36
+
37
+ # @return [Array<TreeStand::Capture>]
38
+ def captures
39
+ @captures ||= @ts_match.captures.map do |capture|
40
+ TreeStand::Capture.new(self, capture)
41
+ end
42
+ end
43
+
44
+ # @param other [Object]
45
+ # @return [bool]
46
+ def ==(other)
47
+ return false unless other.is_a?(TreeStand::Match)
48
+
49
+ captures == other.captures
50
+ end
51
+
52
+ # @return [TreeStand::Capture, nil]
53
+ def dig(name, *)
54
+ self[name]
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,76 @@
1
+ module TreeStand
2
+ # Wrapper around a TreeSitter node and provides convient
3
+ # methods that are missing on the original node. This class
4
+ # overrides the `method_missing` method to delegate to a nodes
5
+ # named children.
6
+ class Node
7
+ extend Forwardable
8
+ include Enumerable
9
+
10
+ def_delegators :@ts_node, :type, :start_byte, :end_byte, :start_point, :end_point
11
+
12
+ # @api private
13
+ def initialize(tree, ts_node)
14
+ @tree = tree
15
+ @ts_node = ts_node
16
+ @fields = @ts_node.each_field.to_a.map(&:first)
17
+ end
18
+
19
+ # @return [TreeStand::Range]
20
+ def range
21
+ TreeStand::Range.new(
22
+ start_byte: @ts_node.start_byte,
23
+ end_byte: @ts_node.end_byte,
24
+ start_point: @ts_node.start_point,
25
+ end_point: @ts_node.end_point,
26
+ )
27
+ end
28
+
29
+ # Node includes enumerable so that you can iterate over the child nodes.
30
+ # @yieldparam child [TreeStand::Node]
31
+ # @return [Enumerator]
32
+ def each
33
+ @ts_node.each do |child|
34
+ yield TreeStand::Node.new(@tree, child)
35
+ end
36
+ end
37
+
38
+ # @return [TreeStand::Node]
39
+ def parent
40
+ TreeStand::Node.new(@tree, @ts_node.parent)
41
+ end
42
+
43
+ # A convience method for getting the text of the node. Each TreeStand Node
44
+ # wraps the parent tree and has access to the source document.
45
+ # @return [String]
46
+ def text
47
+ @tree.document[@ts_node.start_byte...@ts_node.end_byte]
48
+ end
49
+
50
+ # This class overrides the `method_missing` method to delegate to the
51
+ # node's named children. This allows you to write code like this:
52
+ # root = tree.root_node
53
+ # child = root.expression
54
+ # @overload method_missing(field_name)
55
+ # @param name [Symbol, String]
56
+ # @return [TreeStand::Node] Child node for the given field name
57
+ # @raise [NoMethodError] Raised if the node does not have child with name `field_name`
58
+ #
59
+ # @overload method_missing(method_name, *args, &block)
60
+ # @raise [NoMethodError]
61
+ def method_missing(method, *args, &block)
62
+ return super unless @fields.include?(method.to_s)
63
+ TreeStand::Node.new(@tree, @ts_node.public_send(method, *args, &block))
64
+ end
65
+
66
+ # @param other [Object]
67
+ # @return [bool]
68
+ def ==(other)
69
+ return false unless other.is_a?(TreeStand::Node)
70
+
71
+ range == other.range &&
72
+ type == other.type &&
73
+ text == other.text
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,42 @@
1
+ module TreeStand
2
+ # Wrapper around the TreeSitter parser. It looks up the parser by filename in
3
+ # the configured parsers directory.
4
+ # @example
5
+ # TreeStand.configure do
6
+ # config.parser_path = "path/to/parser/folder/"
7
+ # end
8
+ #
9
+ # # Looks for a parser in `path/to/parser/folder/sql.{so,dylib}`
10
+ # sql_parser = TreeStand::Parser.new("sql")
11
+ #
12
+ # # Looks for a parser in `path/to/parser/folder/ruby.{so,dylib}`
13
+ # ruby_parser = TreeStand::Parser.new("ruby")
14
+ class Parser
15
+ # @return [TreeSitter::Language]
16
+ attr_reader :ts_language
17
+ # @return [TreeSitter::Parser]
18
+ attr_reader :ts_parser
19
+
20
+ # @param language [String]
21
+ def initialize(language)
22
+ @language_string = language
23
+ @ts_language = TreeSitter::Language.load(
24
+ language,
25
+ "#{TreeStand.config.parser_path}/#{language}.so"
26
+ )
27
+ @ts_parser = TreeSitter::Parser.new.tap do |parser|
28
+ parser.language = @ts_language
29
+ end
30
+ end
31
+
32
+ # Parse the provided document with the TreeSitter parser.
33
+ # @param tree [TreeStand::Tree]
34
+ # @param document [String]
35
+ # @return [TreeStand::Tree]
36
+ def parse_string(tree, document)
37
+ # There's a bug with passing a non-nil tree
38
+ ts_tree = @ts_parser.parse_string(nil, document)
39
+ TreeStand::Tree.new(self, ts_tree, document)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,40 @@
1
+ module TreeStand
2
+ # Wrapper around a TreeSitter range. This is mainly used to compare ranges.
3
+ class Range
4
+ # Point is a Struct containing the row and column from a TreeSitter point.
5
+ # TreeStand uses this to compare points.
6
+ # @!attribute [rw] row
7
+ # @return [Integer]
8
+ # @!attribute [rw] column
9
+ # @return [Integer]
10
+ Point = Struct.new(:row, :column)
11
+
12
+ # @return [Integer]
13
+ attr_reader :start_byte
14
+ # @return [Integer]
15
+ attr_reader :end_byte
16
+ # @return [TreeStand::Range::Point]
17
+ attr_reader :start_point
18
+ # @return [TreeStand::Range::Point]
19
+ attr_reader :end_point
20
+
21
+ # @api private
22
+ def initialize(start_byte:, end_byte:, start_point:, end_point:)
23
+ @start_byte = start_byte
24
+ @end_byte = end_byte
25
+ @start_point = Point.new(start_point.row, start_point.column)
26
+ @end_point = Point.new(end_point.row, end_point.column)
27
+ end
28
+
29
+ # @param other [Object]
30
+ # @return [bool]
31
+ def ==(other)
32
+ return false unless other.is_a?(TreeStand::Range)
33
+
34
+ @start_byte == other.start_byte &&
35
+ @end_byte == other.end_byte &&
36
+ @start_point == other.start_point &&
37
+ @end_point == other.end_point
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,95 @@
1
+ module TreeStand
2
+ # Wrapper around a TreeSitter tree.
3
+ #
4
+ # This class exposes a convient API for working with the tree. There are
5
+ # dangers in using this class. The tree is mutable and the document can be
6
+ # changed. This class does not protect against that.
7
+ #
8
+ # Some of the moetods on this class edit and re-parse the document updating
9
+ # the tree. Because the document is re-parsed, the tree will be different. Which
10
+ # means all outstanding nodes & ranges will be invalid.
11
+ #
12
+ # Methods that edit the document are suffixed with `!`, e.g. `#edit!`.
13
+ #
14
+ # It's often the case that you will want perfrom multiple edits. One such
15
+ # pattern is to call #query & #edit on all matches in a loop. It's important
16
+ # to keep the destructive nature of #edit in mind and re-issue the query
17
+ # after each edit.
18
+ #
19
+ # Another thing to keep in mind is that edits done later in the document will
20
+ # likely not affect the ranges that occur earlier in the document. This can
21
+ # be a convient property that could allow you to apply edits in a reverse order.
22
+ # This is not always possible and depends on the edits you make, beware that
23
+ # the tree will be different after each edit and this approach may cause bugs.
24
+ class Tree
25
+ # @return [String]
26
+ attr_reader :document
27
+ # @return [TreeSitter::Tree]
28
+ attr_reader :ts_tree
29
+
30
+ # @api private
31
+ def initialize(parser, tree, document)
32
+ @parser = parser
33
+ @ts_tree = tree
34
+ @document = document
35
+ end
36
+
37
+ # @return [TreeStand::Node]
38
+ def root_node
39
+ TreeStand::Node.new(self, @ts_tree.root_node)
40
+ end
41
+
42
+ # TreeSitter uses a `TreeSitter::Cursor` to iterate over matches by calling
43
+ # `curser#next_match` repeatedly until it returns `nil`.
44
+ #
45
+ # This method does all of that for you and collects them into an array.
46
+ #
47
+ # @see TreeStand::Match
48
+ # @see TreeStand::Capture
49
+ #
50
+ # @param query_string [String]
51
+ # @return [Array<TreeStand::Match>]
52
+ def query(query_string)
53
+ ts_query = TreeSitter::Query.new(@parser.ts_language, query_string)
54
+ ts_cursor = TreeSitter::QueryCursor.exec(ts_query, @ts_tree.root_node)
55
+ matches = []
56
+ while match = ts_cursor.next_match
57
+ matches << TreeStand::Match.new(self, ts_query, match)
58
+ end
59
+ matches
60
+ end
61
+
62
+ # This method replaces the section of the document specified by range and
63
+ # replaces it with the provided text. Then it will reparse the document and
64
+ # update the tree!
65
+ # @param range [TreeStand::Range]
66
+ # @param replacement [String]
67
+ # @return [void]
68
+ def edit!(range, replacement)
69
+ new_document = +""
70
+ new_document << @document[0...range.start_byte]
71
+ new_document << replacement
72
+ new_document << @document[range.end_byte..-1]
73
+ replace_with_new_doc(new_document)
74
+ end
75
+
76
+ # This method deletes the section of the document specified by range Then
77
+ # it will reparse the document and update the tree!
78
+ # @param range [TreeStand::Range]
79
+ # @return [void]
80
+ def delete!(range)
81
+ new_document = +""
82
+ new_document << @document[0...range.start_byte]
83
+ new_document << @document[range.end_byte..-1]
84
+ replace_with_new_doc(new_document)
85
+ end
86
+
87
+ private
88
+
89
+ def replace_with_new_doc(new_document)
90
+ @document = new_document
91
+ new_tree = @parser.parse_string(@ts_tree, @document)
92
+ @ts_tree = new_tree.ts_tree
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,4 @@
1
+ module TreeStand
2
+ # The current version of the gem.
3
+ VERSION = "0.1.0"
4
+ end
@@ -0,0 +1,59 @@
1
+ module TreeStand
2
+ # Depth-first traversal through the tree, calling hooks at each stop.
3
+ #
4
+ # Hooks are language depended so are defined by creating methods on the
5
+ # visitor with the form `on_#{node.type}`.
6
+ #
7
+ # You can also define an `_on_default` method to run on all nodes.
8
+ #
9
+ # @example Create a visitor counting certain nodes
10
+ # class CountingVisitor < TreeStand::Visitor
11
+ # attr_reader :count
12
+ #
13
+ # def initialize(document, type:)
14
+ # super(document)
15
+ # @type = type
16
+ # @count = 0
17
+ # end
18
+ #
19
+ # def on_predicate(node)
20
+ # # if this node matches our search, increment the counter
21
+ # @count += 1 if node.type == @type
22
+ # end
23
+ # end
24
+ #
25
+ # # Initialize a visitor
26
+ # visitor = CountingVisitor.new(document, :predicate).visit
27
+ # # Check the result
28
+ # visitor.count
29
+ # # => 3
30
+ class Visitor
31
+ # @param node [TreeStand::Node]
32
+ def initialize(node)
33
+ @node = node
34
+ end
35
+
36
+ # Run the visitor on the document and return self. Allows chaining create and visit.
37
+ # @example
38
+ # visitor = CountingVisitor.new(node, :predicate).visit
39
+ # @return [self]
40
+ def visit
41
+ visit_node(@node)
42
+ self
43
+ end
44
+
45
+ private
46
+
47
+ def visit_node(node)
48
+ if respond_to?("on_#{node.type}")
49
+ public_send("on_#{node.type}", node)
50
+ elsif respond_to?(:_on_default)
51
+ _on_default(node)
52
+ end
53
+
54
+ node.each do |child|
55
+ visit_node(child)
56
+ end
57
+ end
58
+ end
59
+ end
data/lib/tree_stand.rb ADDED
@@ -0,0 +1,33 @@
1
+ require "tree_sitter"
2
+ require "zeitwerk"
3
+
4
+ loader = Zeitwerk::Loader.for_gem
5
+ loader.setup
6
+
7
+ # TreeStand is a high-level Ruby wrapper for {https://tree-sitter.github.io/tree-sitter tree-sitter} bindings. It makes
8
+ # it easier to configure the parsers, and work with the underlying syntax tree.
9
+ module TreeStand
10
+ # Common Ancestor for all TreeStand errors.
11
+ class Error < StandardError; end
12
+
13
+ class << self
14
+ # Easy configuration of the gem.
15
+ #
16
+ # @example
17
+ # TreeStand.configure do
18
+ # config.parser_path = "path/to/parser/folder/"
19
+ # end
20
+ #
21
+ # sql_parser = TreeStand::Parser.new("sql")
22
+ # ruby_parser = TreeStand::Parser.new("ruby")
23
+ # @return [void]
24
+ def configure(&block)
25
+ instance_eval(&block)
26
+ end
27
+
28
+ # @return [TreeStand::Config]
29
+ def config
30
+ @config ||= Config.new
31
+ end
32
+ end
33
+ end
data/parsers/.gitkeep ADDED
File without changes
data/service.yml ADDED
@@ -0,0 +1 @@
1
+ classification: library
@@ -0,0 +1,3 @@
1
+ dependencies:
2
+ pre:
3
+ - ./deploy/install-treesitter
@@ -0,0 +1,36 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "tree_stand/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "tree_stand"
7
+ spec.version = TreeStand::VERSION
8
+ spec.authors = ["derekstride"]
9
+ spec.email = ["derek@stride.host", "opensource@shopify.com"]
10
+
11
+ spec.summary = "A high-level Ruby wrapper for the Tree-sitter bindings"
12
+ spec.homepage = "https://github.com/Shopify/tree_stand"
13
+ spec.license = "MIT"
14
+
15
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = spec.homepage
19
+ spec.metadata["changelog_uri"] = spec.homepage
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
24
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25
+ end
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_dependency "zeitwerk"
31
+
32
+ spec.add_development_dependency "bundler", "~> 2.3"
33
+ spec.add_development_dependency "rake", "~> 10.0"
34
+ spec.add_development_dependency "minitest"
35
+ spec.add_development_dependency "pry-byebug"
36
+ end
metadata ADDED
@@ -0,0 +1,146 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tree_stand
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - derekstride
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-12-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: zeitwerk
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.3'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry-byebug
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description:
84
+ email:
85
+ - derek@stride.host
86
+ - opensource@shopify.com
87
+ executables: []
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - ".github/workflows/ci.yml"
92
+ - ".github/workflows/cla.yml"
93
+ - ".gitignore"
94
+ - ".shopify-build/VERSION"
95
+ - ".shopify-build/tree-stand-publish-package.yml"
96
+ - CODE_OF_CONDUCT.md
97
+ - Gemfile
98
+ - LICENSE.txt
99
+ - README.md
100
+ - Rakefile
101
+ - bin/console
102
+ - bin/setup
103
+ - deploy/install-treesitter
104
+ - lib/tree_stand.rb
105
+ - lib/tree_stand/ast_modifier.rb
106
+ - lib/tree_stand/capture.rb
107
+ - lib/tree_stand/config.rb
108
+ - lib/tree_stand/match.rb
109
+ - lib/tree_stand/node.rb
110
+ - lib/tree_stand/parser.rb
111
+ - lib/tree_stand/range.rb
112
+ - lib/tree_stand/tree.rb
113
+ - lib/tree_stand/version.rb
114
+ - lib/tree_stand/visitor.rb
115
+ - parsers/.gitkeep
116
+ - service.yml
117
+ - shipit.production.yml
118
+ - tree_stand.gemspec
119
+ homepage: https://github.com/Shopify/tree_stand
120
+ licenses:
121
+ - MIT
122
+ metadata:
123
+ allowed_push_host: https://rubygems.org
124
+ homepage_uri: https://github.com/Shopify/tree_stand
125
+ source_code_uri: https://github.com/Shopify/tree_stand
126
+ changelog_uri: https://github.com/Shopify/tree_stand
127
+ post_install_message:
128
+ rdoc_options: []
129
+ require_paths:
130
+ - lib
131
+ required_ruby_version: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - ">="
134
+ - !ruby/object:Gem::Version
135
+ version: '0'
136
+ required_rubygems_version: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - ">="
139
+ - !ruby/object:Gem::Version
140
+ version: '0'
141
+ requirements: []
142
+ rubygems_version: 3.3.3
143
+ signing_key:
144
+ specification_version: 4
145
+ summary: A high-level Ruby wrapper for the Tree-sitter bindings
146
+ test_files: []