diff options
author | Kevin Newton <[email protected]> | 2024-08-12 14:22:21 -0400 |
---|---|---|
committer | git <[email protected]> | 2024-08-12 18:44:18 +0000 |
commit | d012f6d49fe9e995bdc6d12e4ebfaa043298d7c2 (patch) | |
tree | 61eb3e057a856bae84601150671b1da04c2129c0 | |
parent | 568d7ab7f5862063107b3643c6cfe77a387f09d6 (diff) |
[ruby/prism] Add sample for generating tags
https://github.com/ruby/prism/commit/7c9ca47ac5
-rw-r--r-- | sample/prism/make_tags.rb | 302 |
1 files changed, 302 insertions, 0 deletions
diff --git a/sample/prism/make_tags.rb b/sample/prism/make_tags.rb new file mode 100644 index 0000000000..dc770ab1b0 --- /dev/null +++ b/sample/prism/make_tags.rb @@ -0,0 +1,302 @@ +# This script generates a tags file using Prism to parse the Ruby files. + +require "prism" + +# This visitor is responsible for visiting the nodes in the AST and generating +# the appropriate tags. The tags are stored in the entries array as strings. +class TagsVisitor < Prism::Visitor + # This represents an entry in the tags file, which is a tab-separated line. It + # houses the logic for how an entry is constructed. + class Entry + attr_reader :parts + + def initialize(name, filepath, pattern, type) + @parts = [name, filepath, pattern, type] + end + + def attribute(key, value) + parts << "#{key}:#{value}" + end + + def attribute_class(nesting, names) + return if nesting.empty? && names.length == 1 + attribute("class", [*nesting, names].flatten.tap(&:pop).join(".")) + end + + def attribute_inherits(names) + attribute("inherits", names.join(".")) if names + end + + def to_line + parts.join("\t") + end + end + + private_constant :Entry + + attr_reader :entries, :filepath, :lines, :nesting, :singleton + + # Initialize the visitor with the given parameters. The first three parameters + # are constant throughout the visit, while the last two are controlled by the + # visitor as it traverses the AST. These are treated as immutable by virtue of + # the visit methods constructing new visitors when they need to change. + def initialize(entries, filepath, lines, nesting = [], singleton = false) + @entries = entries + @filepath = filepath + @lines = lines + @nesting = nesting + @singleton = singleton + end + + # Visit a method alias node and generate the appropriate tags. + # + # alias m2 m1 + # + def visit_alias_method_node(node) + enter(node.new_name.unescaped.to_sym, node, "a") do |entry| + entry.attribute_class(nesting, [nil]) + end + + super + end + + # Visit a method call to attr_reader, attr_writer, or attr_accessor without a + # receiver and generate the appropriate tags. Note that this ignores the fact + # that these methods could be overridden, which is a limitation of this + # script. + # + # attr_accessor :m1 + # + def visit_call_node(node) + if !node.receiver && %i[attr_reader attr_writer attr_accessor].include?(name = node.name) + (node.arguments&.arguments || []).grep(Prism::SymbolNode).each do |argument| + if name != :attr_writer + enter(:"#{argument.unescaped}", argument, singleton ? "F" : "f") do |entry| + entry.attribute_class(nesting, [nil]) + end + end + + if name != :attr_reader + enter(:"#{argument.unescaped}=", argument, singleton ? "F" : "f") do |entry| + entry.attribute_class(nesting, [nil]) + end + end + end + end + + super + end + + # Visit a class node and generate the appropriate tags. + # + # class C1 + # end + # + def visit_class_node(node) + if (names = names_for(node.constant_path)) + enter(names.last, node, "c") do |entry| + entry.attribute_class(nesting, names) + entry.attribute_inherits(names_for(node.superclass)) + end + + node.body&.accept(copy_visitor([*nesting, names], singleton)) + end + end + + # Visit a constant path write node and generate the appropriate tags. + # + # C1::C2 = 1 + # + def visit_constant_path_write_node(node) + if (names = names_for(node.target)) + enter(names.last, node, "C") do |entry| + entry.attribute_class(nesting, names) + end + end + + super + end + + # Visit a constant write node and generate the appropriate tags. + # + # C1 = 1 + # + def visit_constant_write_node(node) + enter(node.name, node, "C") do |entry| + entry.attribute_class(nesting, [nil]) + end + + super + end + + # Visit a method definition node and generate the appropriate tags. + # + # def m1; end + # + def visit_def_node(node) + enter(node.name, node, (node.receiver || singleton) ? "F" : "f") do |entry| + entry.attribute_class(nesting, [nil]) + end + + super + end + + # Visit a module node and generate the appropriate tags. + # + # module M1 + # end + # + def visit_module_node(node) + if (names = names_for(node.constant_path)) + enter(names.last, node, "m") do |entry| + entry.attribute_class(nesting, names) + end + + node.body&.accept(copy_visitor([*nesting, names], singleton)) + end + end + + # Visit a singleton class node and generate the appropriate tags. + # + # class << self + # end + # + def visit_singleton_class_node(node) + case node.expression + when Prism::SelfNode + node.body&.accept(copy_visitor(nesting, true)) + when Prism::ConstantReadNode, Prism::ConstantPathNode + if (names = names_for(node.expression)) + node.body&.accept(copy_visitor([*nesting, names], true)) + end + else + node.body&.accept(copy_visitor([*nesting, nil], true)) + end + end + + private + + # Generate a new visitor with the given dynamic options. The static options + # are copied over automatically. + def copy_visitor(nesting, singleton) + TagsVisitor.new(entries, filepath, lines, nesting, singleton) + end + + # Generate a new entry for the given name, node, and type and add it into the + # list of entries. The block is used to add additional attributes to the + # entry. + def enter(name, node, type) + line = lines[node.location.start_line - 1].chomp + pattern = "/^#{line.gsub("\\", "\\\\\\\\").gsub("/", "\\/")}$/;\"" + + entry = Entry.new(name, filepath, pattern, type) + yield entry + + entries << entry.to_line + end + + # Retrieve the names for the given node. This is used to construct the class + # attribute for the tags. + def names_for(node) + case node + when Prism::ConstantPathNode + names = names_for(node.parent) + return unless names + + names << node.name + when Prism::ConstantReadNode + [node.name] + when Prism::SelfNode + [:self] + else + # dynamic + end + end +end + +# Parse the Ruby file and visit all of the nodes in the resulting AST. Once all +# of the nodes have been visited, the entries array should be populated with the +# tags. +result = Prism.parse_stream(DATA) +result.value.accept(TagsVisitor.new(entries = [], __FILE__, result.source.lines)) + +# Print the tags to STDOUT. +puts "!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;\" to lines/" +puts "!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/" +puts entries.sort + +# => +# !_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +# !_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +# C1 sample/prism/make_tags.rb /^ class C1$/;" c class:M1.M2 +# C2 sample/prism/make_tags.rb /^ class C2 < Object$/;" c class:M1.M2.C1 inherits:Object +# C6 sample/prism/make_tags.rb /^ C6 = 1$/;" C class:M1 +# C7 sample/prism/make_tags.rb /^ C7 = 2$/;" C class:M1 +# C9 sample/prism/make_tags.rb /^ C8::C9 = 3$/;" C class:M1.C8 +# M1 sample/prism/make_tags.rb /^module M1$/;" m +# M2 sample/prism/make_tags.rb /^ module M2$/;" m class:M1 +# M4 sample/prism/make_tags.rb /^ module M3::M4$/;" m class:M1.M3 +# M5 sample/prism/make_tags.rb /^ module self::M5$/;" m class:M1.self +# m1 sample/prism/make_tags.rb /^ def m1; end$/;" f class:M1.M2.C1.C2 +# m10 sample/prism/make_tags.rb /^ attr_accessor :m10, :m11$/;" f class:M1.M3.M4 +# m10= sample/prism/make_tags.rb /^ attr_accessor :m10, :m11$/;" f class:M1.M3.M4 +# m11 sample/prism/make_tags.rb /^ attr_accessor :m10, :m11$/;" f class:M1.M3.M4 +# m11= sample/prism/make_tags.rb /^ attr_accessor :m10, :m11$/;" f class:M1.M3.M4 +# m12 sample/prism/make_tags.rb /^ attr_reader :m12, :m13, :m14$/;" f class:M1.M3.M4 +# m13 sample/prism/make_tags.rb /^ attr_reader :m12, :m13, :m14$/;" f class:M1.M3.M4 +# m14 sample/prism/make_tags.rb /^ attr_reader :m12, :m13, :m14$/;" f class:M1.M3.M4 +# m15= sample/prism/make_tags.rb /^ attr_writer :m15$/;" f class:M1.M3.M4 +# m2 sample/prism/make_tags.rb /^ def m2; end$/;" f class:M1.M2.C1.C2 +# m3 sample/prism/make_tags.rb /^ alias m3 m1$/;" a class:M1.M2.C1.C2 +# m4 sample/prism/make_tags.rb /^ alias :m4 :m2$/;" a class:M1.M2.C1.C2 +# m5 sample/prism/make_tags.rb /^ def self.m5; end$/;" F class:M1.M2.C1.C2 +# m6 sample/prism/make_tags.rb /^ def m6; end$/;" F class:M1.M2.C1.C2 +# m7 sample/prism/make_tags.rb /^ def m7; end$/;" F class:M1.M2.C1.C2.C3 +# m8 sample/prism/make_tags.rb /^ def m8; end$/;" F class:M1.M2.C1.C2.C4.C5 +# m9 sample/prism/make_tags.rb /^ def m9; end$/;" F class:M1.M2.C1.C2. + +__END__ +module M1 + module M2 + class C1 + class C2 < Object + def m1; end + def m2; end + + alias m3 m1 + alias :m4 :m2 + + def self.m5; end + + class << self + def m6; end + end + + class << C3 + def m7; end + end + + class << C4::C5 + def m8; end + end + + class << c + def m9; end + end + end + end + end + + module M3::M4 + attr_accessor :m10, :m11 + attr_reader :m12, :m13, :m14 + attr_writer :m15 + end + + module self::M5 + end + + C6 = 1 + C7 = 2 + C8::C9 = 3 +end |