Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,23 +213,34 @@ Oxc.parse("let a; let a;", semantic_errors: true).errors.map(&:message)
root = Oxc.parse(source).root

root.type #=> "Program"
root.child_nodes #=> the nodes directly under it
root.every("Identifier") #=> every identifier in the file
root.at(offset) #=> the innermost node covering a byte offset
root.each #=> an Enumerator over every node
```

An ESTree field always wins over a method of the gem's own, since `name`, `attributes` and `children` are all real fields. `Identifier#name` is the identifier's name, `JSXElement#children` is what the element wraps, and `ImportDeclaration#attributes` is the import's `with` clause. The walker spells its own versions `underscored_type`, `to_h` and `child_nodes`, which no ESTree field can be called.

A field comes back as a node when it holds one, so reads chain.

```ruby
declaration = root.children.first
declaration = root.child_nodes.first

declaration.kind
#=> "let"

declaration.declarations.first.id["name"]
declaration.declarations.first.id.name
#=> "count"
```

ESTree names its fields in camelCase, and a field answers to its snake_case name too, so reading an AST does not mean writing JavaScript casing in Ruby.

```ruby
node.type_annotation # the same field as node.typeAnnotation
node.super_class # superClass
root.source_type # sourceType
```

`ancestors` is what a rewrite needs, since a reference sits inside the expression that has to be replaced.

```ruby
Expand Down
45 changes: 32 additions & 13 deletions lib/oxc/node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,14 @@ class Node
SPAN = ["type", "start", "end"].freeze #: Array[String]
SCALARS = 3 #: Integer

attr_reader :attributes #: Hash[String, untyped]
attr_reader :parent #: Oxc::Node?

protected

attr_reader :attributes #: Hash[String, untyped]

public

#: (Hash[String, untyped], ?Oxc::Node?) -> void
def initialize(attributes, parent = nil)
@attributes = attributes
Expand All @@ -34,27 +39,32 @@ def finish
end

#: () -> String
def name
@name ||= type.gsub(ACRONYM, '\1_\2').gsub(BOUNDARY, '\1_\2').downcase
def underscored_type
@underscored_type ||= type.gsub(ACRONYM, '\1_\2').gsub(BOUNDARY, '\1_\2').downcase
end

#: (String) -> untyped
def [](key)
attributes[key]
end

#: () -> Hash[String, untyped]
def to_h
attributes
end

#: (String) -> String?
def slice(source)
source.byteslice(start, finish - start)
end

#: () -> Array[Oxc::Node]
def children
@children ||= attributes.each_value
.flat_map { |value| value.is_a?(Array) ? value : [value] }
.select { |value| value.is_a?(Hash) && value.key?("type") }
.map { |value| Node.new(value, self) }
.freeze
def child_nodes
@child_nodes ||= attributes.each_value
.flat_map { |value| value.is_a?(Array) ? value : [value] }
.select { |value| value.is_a?(Hash) && value.key?("type") }
.map { |value| Node.new(value, self) }
.freeze
end

#: () { (Oxc::Node) -> void } -> void
Expand All @@ -64,7 +74,7 @@ def each(&)

yield self

children.each { |child| child.each(&) }
child_nodes.each { |child| child.each(&) }
end

#: () -> Array[Oxc::Node]
Expand Down Expand Up @@ -99,20 +109,29 @@ def inspect

#: (Symbol, *untyped) -> untyped
def method_missing(name, *arguments)
key = name.to_s
key = field_for(name.to_s)

return super unless attributes.key?(key)
return super unless key

wrap(attributes[key])
end

#: (Symbol, ?bool) -> bool
def respond_to_missing?(name, include_private = false)
attributes.key?(name.to_s) || super
!field_for(name.to_s).nil? || super
end

private

#: (String) -> String?
def field_for(name)
return name if attributes.key?(name)

camelized = name.gsub(/_([a-z\d])/) { Regexp.last_match(1).to_s.upcase }

camelized if attributes.key?(camelized)
end

#: (untyped) -> untyped
def wrap(value)
case value
Expand Down
4 changes: 2 additions & 2 deletions lib/oxc/visitor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def visit(node)

return nil unless node

answer = "visit_#{node.name}"
answer = "visit_#{node.underscored_type}"

respond_to?(answer) ? public_send(answer, node) : visit_children(node)

Expand All @@ -17,7 +17,7 @@ def visit(node)

#: (Oxc::Node) -> void
def visit_children(node)
node.children.each { |child| visit(child) }
node.child_nodes.each { |child| visit(child) }

nil
end
Expand Down
14 changes: 11 additions & 3 deletions sig/oxc/node.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ module Oxc

SCALARS: Integer

attr_reader parent: Oxc::Node?

attr_reader attributes: Hash[String, untyped]

attr_reader parent: Oxc::Node?
public

# : (Hash[String, untyped], ?Oxc::Node?) -> void
def initialize: (Hash[String, untyped], ?Oxc::Node?) -> void
Expand All @@ -29,16 +31,19 @@ module Oxc
def finish: () -> Integer

# : () -> String
def name: () -> String
def underscored_type: () -> String

# : (String) -> untyped
def []: (String) -> untyped

# : () -> Hash[String, untyped]
def to_h: () -> Hash[String, untyped]

# : (String) -> String?
def slice: (String) -> String?

# : () -> Array[Oxc::Node]
def children: () -> Array[Oxc::Node]
def child_nodes: () -> Array[Oxc::Node]

# : () { (Oxc::Node) -> void } -> void
# : () -> Enumerator[Oxc::Node, void]
Expand Down Expand Up @@ -68,6 +73,9 @@ module Oxc

private

# : (String) -> String?
def field_for: (String) -> String?

# : (untyped) -> untyped
def wrap: (untyped) -> untyped
end
Expand Down
73 changes: 65 additions & 8 deletions test/oxc/node_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ def root
end

test "names itself the way a visitor answers it" do
assert_equal "program", root.name
assert_equal "variable_declaration", root.children.first.name
assert_equal "program", root.underscored_type
assert_equal "variable_declaration", root.child_nodes.first.underscored_type
end

test "reads an acronym as one word" do
assert_equal "ts_type_annotation", Node.new({ "type" => "TSTypeAnnotation" }).name
assert_equal "jsx_element", Node.new({ "type" => "JSXElement" }).name
assert_equal "ts_type_annotation", Node.new({ "type" => "TSTypeAnnotation" }).underscored_type
assert_equal "jsx_element", Node.new({ "type" => "JSXElement" }).underscored_type
end

test "walks everything under it" do
Expand All @@ -42,15 +42,72 @@ def root
end

test "reads a field as the node it holds" do
declarator = root.children.first.declarations.first
declarator = root.child_nodes.first.declarations.first

assert_equal "Identifier", declarator.id.type
assert_equal "count", declarator.id["name"]
assert_equal 0, declarator.init["value"]
end

test "reads a field that is not a node as it is" do
assert_equal "let", root.children.first.kind
assert_equal "let", root.child_nodes.first.kind
end

test "answers the ESTree field and not a method of its own" do
identifier = root.every("Identifier").first

assert_equal "count", identifier.name
assert_equal "identifier", identifier.underscored_type
end

test "answers the ESTree field on a node carrying children of its own" do
element = Oxc.parse("<div><span/></div>", filename: "App.jsx").root.every("JSXElement").first

assert_equal ["JSXElement"], element.children.map(&:type)
assert_equal ["JSXOpeningElement", "JSXElement", "JSXClosingElement"], element.child_nodes.map(&:type)
end

test "answers the ESTree field on a node carrying attributes of its own" do
opening = Oxc.parse(%(<div id="a" />), filename: "App.jsx").root.every("JSXOpeningElement").first

assert_equal(["id"], opening.attributes.map { |attribute| attribute.name.name })
end

test "answers import attributes, which name a field the walker once shadowed" do
source = %(import a from "./a" with { type: "json" })
declaration = Oxc.parse(source, source_type: "module").root.child_nodes.first

assert_equal(["type"], declaration.attributes.map { |attribute| attribute.key.name })
end

test "reads a camelCase field by its snake_case name" do
source = %(class C extends B { declare readonly x?: T })
declaration = Oxc.parse(source, filename: "a.ts").root.every("PropertyDefinition").first

assert_equal "TSTypeAnnotation", declaration.type_annotation.type
assert_equal declaration.typeAnnotation.type, declaration.type_annotation.type
assert_equal "B", Oxc.parse(source, filename: "a.ts").root.every("ClassDeclaration").first.super_class.name
assert_equal "module", root.source_type
end

test "answers to a field by either name" do
declaration = Oxc.parse("class C { x?: T }", filename: "a.ts").root.every("PropertyDefinition").first

assert_respond_to declaration, :type_annotation
assert_respond_to declaration, :typeAnnotation
refute_respond_to declaration, :type_nonsense
end

test "leaves a method of its own alone, whatever a field is called" do
assert_equal "program", root.underscored_type
assert_equal 2, root.child_nodes.length
end

test "answers its own fields as a hash" do
node = root.every("VariableDeclarator").first

assert_equal ["type", "id", "init", "start", "end"], node.to_h.keys
assert_equal "VariableDeclarator", node.to_h["type"]
end

test "refuses a field it does not carry" do
Expand Down Expand Up @@ -90,11 +147,11 @@ def root
test "prints what it is, and what it carries in its own right" do
assert_equal %(#<Oxc::Node Program 0..#{SOURCE.bytesize} sourceType="module">), root.inspect
assert_equal %(#<Oxc::Node Identifier 4..9 name="count">), root.every("Identifier").first.inspect
assert_equal %(#<Oxc::Node VariableDeclaration 0..13 kind="let">), root.children.first.inspect
assert_equal %(#<Oxc::Node VariableDeclaration 0..13 kind="let">), root.child_nodes.first.inspect
end

test "leaves the nodes hanging off it out of what it carries" do
declaration = root.children.first
declaration = root.child_nodes.first

assert_equal ["kind"], declaration.scalars.keys
end
Expand Down