masamune-ast 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1131c8f9da2cb862e77f3840c9abc806443a7d4445d788fd3683cec41b1916e8
4
+ data.tar.gz: bb9025361d1063fd3f7776070830b4e41b7529d3ea04b11fbe9466890bca8d28
5
+ SHA512:
6
+ metadata.gz: f6e571ac354ab0fae7e39fbf1d0030c1025738732d008b9c3afa9ade11c2cb41b88d67a08ac66d878dd913fae1a9cf08f79d4539f931b7a55fd43f563332d08b
7
+ data.tar.gz: 47065f76eebaab82601b9d577a738765e66d588f6ad300931223bd61824e0f290612a6341dd098d8bd78cdb90e5b44ce9513d6beb5280b9ebc5691b4044145ea
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2023-04-24
4
+
5
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in masamune.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "minitest", "~> 5.0"
11
+
12
+ gem "pry"
data/Gemfile.lock ADDED
@@ -0,0 +1,27 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ masamune-ast (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ coderay (1.1.3)
10
+ method_source (1.0.0)
11
+ minitest (5.18.0)
12
+ pry (0.14.2)
13
+ coderay (~> 1.1)
14
+ method_source (~> 1.0)
15
+ rake (13.0.6)
16
+
17
+ PLATFORMS
18
+ x86_64-linux
19
+
20
+ DEPENDENCIES
21
+ masamune-ast!
22
+ minitest (~> 5.0)
23
+ pry
24
+ rake (~> 13.0)
25
+
26
+ BUNDLED WITH
27
+ 2.4.1
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Gabriel Zayas
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,66 @@
1
+ # MasamuneAst
2
+
3
+ ## A Ruby source code analyzer based on Ripper’s Abstract Syntax Tree generator (`Ripper#sexp`).
4
+
5
+ ## Installation
6
+
7
+ ```ruby
8
+ gem "masamune-ast"
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ Pinpoint variables and methods in your source code even when other tokens have the same or similar spelling:
14
+ ```ruby
15
+ code = <<CODE
16
+ java = "java"
17
+ javascript = java + "script"
18
+ puts java + " is not " + javascript
19
+ # java
20
+ CODE
21
+
22
+ msmn = MasamuneAst::AbstractSyntaxTree.new(code)
23
+
24
+ msmn.variables
25
+ [[[1, 0], "java"], [[2, 0], "javascript"], [[2, 13], "java"]]
26
+
27
+ msmn.search(:variable, "java")
28
+ #=> [[[1, 0], "java"], [[2, 13], "java"]]
29
+ ```
30
+
31
+ In some cases, it can be easier to look at the given lex nodes to analyze your source code:
32
+ ```ruby
33
+ msmn.lex_nodes
34
+ => [#<Masamune::LexNode:0x00007fd61810cac0 @ast_id=1200, @index=0, @position=[1, 0], @state=CMDARG, @token="java", @type=:ident>,
35
+ #<Masamune::LexNode:0x00007fd61810c930 @ast_id=1200, @index=1, @position=[1, 4], @state=CMDARG, @token=" ", @type=:sp>,
36
+ #<Masamune::LexNode:0x00007fd61810c7c8 @ast_id=1200, @index=2, @position=[1, 5], @state=BEG, @token="=", @type=:op>,
37
+ #<Masamune::LexNode:0x00007fd61810c638 @ast_id=1200, @index=3, @position=[1, 6], @state=BEG, @token=" ", @type=:sp>,
38
+ #<Masamune::LexNode:0x00007fd61810c480 @ast_id=1200, @index=4, @position=[1, 7], @state=BEG, @token="\"", @type=:tstring_beg>,
39
+ #<Masamune::LexNode:0x00007fd61810c318 @ast_id=1200, @index=5, @position=[1, 8], @state=BEG, @token="java", @type=:tstring_content>,
40
+ #<Masamune::LexNode:0x00007fd61810c188 @ast_id=1200, @index=6, @position=[1, 12], @state=END, @token="\"", @type=:tstring_end>,
41
+ #<Masamune::LexNode:0x00007fd61810c020 @ast_id=1200, @index=7, @position=[1, 13], @state=BEG, @token="\n", @type=:nl>,
42
+ #<Masamune::LexNode:0x00007fd618113e88 @ast_id=1200, @index=8, @position=[2, 0], @state=CMDARG, @token="javascript", @type=:ident>,
43
+ #<Masamune::LexNode:0x00007fd618113cf8 @ast_id=1200, @index=9, @position=[2, 10], @state=CMDARG, @token=" ", @type=:sp>,
44
+ #<Masamune::LexNode:0x00007fd618113b68 @ast_id=1200, @index=10, @position=[2, 11], @state=BEG, @token="=", @type=:op>,
45
+
46
+ ]
47
+
48
+ msmn.lex_nodes[8].is_variable?
49
+ #=> true
50
+
51
+ msmn.lex_nodes[8].is_string?
52
+ #=> false
53
+
54
+ msmn.lex_nodes[8].is_method_definition?
55
+ #=> false
56
+
57
+ # etc...
58
+ ```
59
+
60
+ ## Contributing
61
+
62
+ Bug reports and pull requests are welcome on GitHub at https://github.com/gazayas/masamune.
63
+
64
+ ## License
65
+
66
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/test_*.rb"]
10
+ end
11
+
12
+ task default: :test
@@ -0,0 +1,11 @@
1
+ # TODO: Not sure if I'll implement this yet.
2
+
3
+ module MasamuneAst
4
+ class AbstractSyntaxTree
5
+ class DataNode << MasamuneAst::AbstractSyntaxTree::Node
6
+ def initialize(tree_node)
7
+ @parent, @contents, _ = tree_node
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,16 @@
1
+ # TODO: Not sure if I'll implement this yet.
2
+
3
+ module MasamuneAst
4
+ class AbstractSyntaxTree
5
+ class Node
6
+ def initialize(tree_node)
7
+ @contents = tree_node
8
+ end
9
+
10
+ # i.e. - [:@ident, "variable_name", [4, 7]]
11
+ def is_data_node?
12
+ @contents[0].is_a?(Symbol) && @contents[1].is_a?(String) && (@contents[2][0].is_a?(Integer) && @contents[2][1].is_a?(Integer))
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,121 @@
1
+ module MasamuneAst
2
+ class AbstractSyntaxTree
3
+ attr_reader :data
4
+ attr_accessor :lex_nodes, :debug
5
+
6
+ def initialize(code)
7
+ @data = Ripper.sexp(code)
8
+ raw_lex_nodes = Ripper.lex(code)
9
+ @lex_nodes = raw_lex_nodes.map do |lex_node|
10
+ MasamuneAst::LexNode.new(raw_lex_nodes.index(lex_node), lex_node, self.__id__)
11
+ end
12
+ @debug = false
13
+ end
14
+
15
+ def tree_nodes
16
+ raise "Set `debug` to `true` to display the tree nodes" unless @debug
17
+ search # Perform search with no conditions.
18
+ end
19
+
20
+ def variables
21
+ search(:variable)
22
+ end
23
+
24
+ def all_methods
25
+ method_definitions + method_calls
26
+ end
27
+
28
+ def method_definitions
29
+ search(:def)
30
+ end
31
+
32
+ def method_calls
33
+ search(:method_call)
34
+ end
35
+
36
+ def strings
37
+ search(:string)
38
+ end
39
+
40
+ def data_nodes
41
+ search(:data_node)
42
+ end
43
+
44
+ def search(type = nil, token = nil, tree_node = self.data, result = [])
45
+ return unless tree_node.is_a?(Array) || (tree_node.is_a?(Array) && tree_node.empty?)
46
+ debug_output(tree_node) if @debug
47
+
48
+ # If the first element is an array, then we're getting all arrays so we just continue the search.
49
+ if tree_node.first.is_a?(Array)
50
+ tree_node.each { |node| search(type, token, node, result) }
51
+ elsif tree_node.first.is_a?(Symbol)
52
+ # TODO: These two if statements bother me a lot.
53
+ # There should be a more effiecient/smart way to do this.
54
+ if has_data_node?(tree_node)
55
+ if (type == :variable && (tree_node.first == :var_field || tree_node.first == :var_ref)) ||
56
+ (type == :string && tree_node.first == :string_content) ||
57
+ (type == :def && tree_node.first == :def) ||
58
+ (type == :method_call && tree_node.first == :vcall)
59
+
60
+ # TODO: AbstractSyntaxTree::DataNode.new(tree_node)
61
+ # For most tree nodes, the data_node is housed in the second element.
62
+ position, data_node_token = data_node_parts(tree_node[1])
63
+
64
+ # Gather all results if token isn't specified.
65
+ result << [position, data_node_token] if token == data_node_token || token.nil?
66
+ end
67
+
68
+ # Continue search for all necessary elements.
69
+ case tree_node.first
70
+ when :def, :command
71
+ tree_node.each { |node| search(type, token, node, result) }
72
+ end
73
+
74
+ # The data nodes in :call nodes are in a different place within the array, so we handle that here.
75
+ # These :call nodes represent methods and chained methods like `[1, 2, 3].sum.times`.
76
+ elsif (type == :method_call && tree_node.first == :call)
77
+ # The method inside the [:call, ...] data node is the last element in the array.
78
+ position, data_node_token = data_node_parts(tree_node.last)
79
+ result << [position, data_node_token] if token == data_node_token || token.nil?
80
+ # The second element is where more :call nodes are nested, so we search it.
81
+ search(type, token, tree_node[1], result)
82
+ else
83
+ # Simply continue the search for all other nodes.
84
+ tree_node.each { |node| search(type, token, node, result) }
85
+ end
86
+ end
87
+
88
+ result
89
+ end
90
+
91
+ private
92
+
93
+ # A data node represents an abstraction in the AST which has details about a specific command.
94
+ # i.e. - [:@ident, "variable_name", [4, 7]]
95
+ # These values are the `type`, `token`, and `position`, respectively.
96
+ # It is simliar to what you see in `Ripper.lex(code)` and `Masamune::Base's @lex_nodes`.
97
+ # Data nodes serve as a base case when recursively searching the AST.
98
+ #
99
+ # The parent node's first element houses the type of action being performed:
100
+ # i.e. - [:assign, [:@ident, "variable_name", [4, 7]]]
101
+ # `has_data_node?` is performed on a parent node.
102
+ def has_data_node?(node)
103
+ node[1].is_a?(Array) && node[1][1].is_a?(String)
104
+ end
105
+
106
+ def data_node_parts(tree_node)
107
+ _, token, position = tree_node
108
+ [position, token]
109
+ end
110
+
111
+ def is_line_position?(tree_node)
112
+ tree_node.size == 2 && tree_node.first.is_a?(Integer) && tree_node.last.is_a?(Integer)
113
+ end
114
+
115
+ def debug_output(tree_node)
116
+ puts "==================================" # TODO: Track the array depth and output the number here.
117
+ puts "=================================="
118
+ p tree_node
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,52 @@
1
+ module MasamuneAst
2
+ # https://docs.ruby-lang.org/en/3.0/Ripper.html#method-c-lex
3
+ #
4
+ # @position: Line number and starting point on the line. i.e. - [4, 7].
5
+ # @type: The type of token. i.e. - :kw (is a keyword).
6
+ # @token: The raw string which represents the actual ruby code. i.e. - "do".
7
+ # @state: Ripper::Lexer::State. i.e. - CMDARG.
8
+ class LexNode
9
+ attr_accessor :position, :type, :token, :state, :ast_id
10
+ attr_reader :index
11
+
12
+ def initialize(index, raw_lex_node, ast_id)
13
+ @index = index
14
+ @position, @type, @token, @state = raw_lex_node
15
+ @type = @type.to_s.gsub(/^[a-z]*_/, "").to_sym
16
+
17
+ # Since the Abstract Syntax Tree can get very large,
18
+ # We just save the id and reference it with `ast` below.
19
+ @ast_id = ast_id
20
+ end
21
+
22
+ def ast
23
+ ObjectSpace._id2ref(@ast_id)
24
+ end
25
+
26
+ def is_variable?
27
+ return false unless is_identifier?
28
+ ast.search(:variable, @token).any?
29
+ end
30
+
31
+ def is_method?
32
+ return false unless is_identifier?
33
+ is_method_definition? || is_method_call?
34
+ end
35
+
36
+ def is_method_definition?
37
+ ast.search(:def, @token).any?
38
+ end
39
+
40
+ def is_method_call?
41
+ ast.search(:method_call, @token).any?
42
+ end
43
+
44
+ def is_identifier?
45
+ type == :ident
46
+ end
47
+
48
+ def is_string?
49
+ type == :tstring_content
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,19 @@
1
+ module MasamuneAst
2
+ module Slicer
3
+ def initialize(ast)
4
+ @ast = ast
5
+ end
6
+
7
+ # TODO
8
+
9
+ # def replace(type, token)
10
+ # @ast.search(type, token)
11
+ # end
12
+
13
+ # def insert_inside_block
14
+ # end
15
+
16
+ # def append_to_block
17
+ # end
18
+ end
19
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MasamuneAst
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "masamune-ast/version"
4
+ require "ripper"
5
+ require "masamune-ast/lex_node"
6
+ require "masamune-ast/abstract_syntax_tree"
7
+
8
+ require "pp"
9
+ require "pry"
10
+
11
+ module MasamuneAst
12
+ end
data/sig/masamune.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Masamune
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: masamune-ast
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Gabriel Zayas
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-04-29 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A layer of abstraction on top of Ripper for handling Abstract Syntax
14
+ Trees in Ruby.
15
+ email:
16
+ - g-zayas@hotmail.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - CHANGELOG.md
22
+ - Gemfile
23
+ - Gemfile.lock
24
+ - LICENSE.txt
25
+ - README.md
26
+ - Rakefile
27
+ - lib/masamune-ast.rb
28
+ - lib/masamune-ast/abstract_syntax_tree.rb
29
+ - lib/masamune-ast/abstract_syntax_tree/data_node.rb
30
+ - lib/masamune-ast/abstract_syntax_tree/node.rb
31
+ - lib/masamune-ast/lex_node.rb
32
+ - lib/masamune-ast/slicer.rb
33
+ - lib/masamune-ast/version.rb
34
+ - sig/masamune.rbs
35
+ homepage: https://www.github.com/gazayas/masamune
36
+ licenses:
37
+ - MIT
38
+ metadata:
39
+ homepage_uri: https://www.github.com/gazayas/masamune
40
+ source_code_uri: https://www.github.com/gazayas/masamune
41
+ changelog_uri: https://www.github.com/gazayas/masamune
42
+ post_install_message:
43
+ rdoc_options: []
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: 2.6.0
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ requirements: []
57
+ rubygems_version: 3.3.7
58
+ signing_key:
59
+ specification_version: 4
60
+ summary: MasamuneAst
61
+ test_files: []