Skip to content

Commit

Permalink
[rail_inspector] Prism Visitor::FrameworkDefault
Browse files Browse the repository at this point in the history
Also apply tweaks to Configuration guide
  • Loading branch information
skipkayhil committed Dec 27, 2024
1 parent 96f13d1 commit 5a9ef5c
Show file tree
Hide file tree
Showing 9 changed files with 116 additions and 127 deletions.
4 changes: 0 additions & 4 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,6 @@ gem "uri", ">= 0.13.1", require: false

gem "prism"

group :lint do
gem "syntax_tree", "6.1.1", require: false
end

group :rubocop do
gem "rubocop", ">= 1.25.1", require: false
gem "rubocop-minitest", require: false
Expand Down
4 changes: 0 additions & 4 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,6 @@ GEM
racc
path_expander (1.1.3)
pg (1.5.8)
prettier_print (1.2.1)
prism (1.2.0)
propshaft (1.1.0)
actionpack (>= 7.0.0)
Expand Down Expand Up @@ -617,8 +616,6 @@ GEM
stringio (3.1.1)
sucker_punch (3.2.0)
concurrent-ruby (~> 1.0)
syntax_tree (6.1.1)
prettier_print (>= 1.2.0)
tailwindcss-rails (3.0.0)
railties (>= 7.0.0)
tailwindcss-ruby
Expand Down Expand Up @@ -743,7 +740,6 @@ DEPENDENCIES
stackprof
stimulus-rails
sucker_punch
syntax_tree (= 6.1.1)
tailwindcss-rails
terser (>= 1.1.4)
thruster
Expand Down
2 changes: 1 addition & 1 deletion guides/source/configuring.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ Below are the default values associated with each target version. In cases of co

- [`config.active_record.postgresql_adapter_decode_dates`](#config-active-record-postgresql-adapter-decode-dates): `true`
- [`config.active_record.validate_migration_timestamps`](#config-active-record-validate-migration-timestamps): `true`
- [`config.active_storage.web_image_content_types`](#config-active-storage-web-image-content-types): `%w[image/png image/jpeg image/gif image/webp]`
- [`config.active_storage.web_image_content_types`](#config-active-storage-web-image-content-types): `%w( image/png image/jpeg image/gif image/webp )`
- [`config.yjit`](#config-yjit): `true`

#### Default Values for Target Version 7.1
Expand Down
2 changes: 1 addition & 1 deletion tools/rail_inspector/lib/rail_inspector/configuring.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def initialize
end

def call(path)
@cache[path] ||= SyntaxTree.parse(SyntaxTree.read(path))
@cache[path] ||= Prism.parse_file(path.to_s).value
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def initialize(checker)
def call
visitor = Visitor::Attribute.new
visitor.visit(app_config_tree)
visitor.attribute_map[APP_CONFIG_CONST]["attr_accessor"]
visitor.attribute_map[APP_CONFIG_CONST][:attr_accessor]
end

private
Expand Down
166 changes: 79 additions & 87 deletions tools/rail_inspector/lib/rail_inspector/visitor/framework_default.rb
Original file line number Diff line number Diff line change
@@ -1,137 +1,129 @@
# frozen_string_literal: true

require "syntax_tree"
require "prism"

require_relative "./hash_to_string"
require_relative "./multiline_to_string"

module RailInspector
module Visitor
class FrameworkDefault
TargetVersionCaseFinder =
SyntaxTree::Search.new(
->(node) do
node in SyntaxTree::Case[
value: SyntaxTree::CallNode[
receiver: SyntaxTree::VarRef[
value: SyntaxTree::Ident[value: "target_version"]
]
]
]
end
)

attr_reader :config_map

def initialize
@config_map = {}
end

def visit(node)
case_node, *others = TargetVersionCaseFinder.scan(node).to_a
raise "#{others.length} other cases?" unless others.empty?
target_version_case = node.breadth_first_search do |n|
n in Prism::CaseNode[
predicate: Prism::CallNode[receiver: Prism::LocalVariableReadNode[name: :target_version]]
]
end

visit_when(case_node.consequent)
target_version_case.conditions.each { |cond| visit_when(cond) }
end

private
def visit_when(node)
version = node.arguments.parts[0].parts[0].value

config_map[version] = VersionedConfig.new.config_for(node.statements)
version = node.conditions[0].unescaped

visit_when(node.consequent) if node.consequent.is_a? SyntaxTree::When
config_map[version] = VersionedConfig.new.tap { |v| v.visit(node.statements) }.configs
end

class VersionedConfig < SyntaxTree::Visitor
class VersionedConfig < Prism::Visitor
attr_reader :configs

def initialize
@configs = {}
@framework_stack = []
end

def config_for(node)
visit(node)
@configs
end
def visit_if_node(node)
unless new_framework = respond_to_framework?(node.predicate)
return visit_child_nodes(node)
end

visit_methods do
def visit_if(node)
unless new_framework = respond_to_framework?(node.predicate)
return super
end
if ENV["STRICT"] && current_framework
raise "Potentially nested framework? Current: '#{current_framework}', found: '#{new_framework}'"
end

if ENV["STRICT"] && current_framework
raise "Potentially nested framework? Current: '#{current_framework}', found: '#{new_framework}'"
end
@framework_stack << new_framework
visit_child_nodes(node)
@framework_stack.pop
end

def visit_call_node(node)
name = node.name.to_s

@framework_stack << new_framework
super
@framework_stack.pop
unless name.end_with? "="
return super
end

def visit_assign(node)
assert_framework(node)

target = SyntaxTree::Formatter.format(nil, node.target)
value =
case node.value
when SyntaxTree::HashLiteral
HashToString.new.tap { |v| v.visit(node.value) }.to_s
when SyntaxTree::StringConcat
MultilineToString.new.tap { |v| v.visit(node.value) }.to_s
else
SyntaxTree::Formatter.format(nil, node.value)
end
@configs[target] = value
handle_assignment(node, name[...-1], node.arguments.arguments[0])
end

def visit_call_or_write_node(node)
name = node.write_name.to_s

unless name.end_with? "="
return super
end

def visit_opassign(node)
if node.operator.name == :"||="
visit_assign(node)
else
super
handle_assignment(node, node.read_name.to_s, node.value)
end

def handle_assignment(node, name, value)
prefix = case node.receiver
in Prism::ConstantReadNode[name: constant_name]
constant_name
in Prism::SelfNode
"self"
in Prism::CallNode[receiver: nil, name: framework]
framework_string = framework.to_s

unless current_framework == framework_string
raise "expected: #{current_framework}, actual: #{framework_string}"
end

framework_string
else
node.receiver.location.slice
end
end

private
def assert_framework(node)
framework =
case node.target.parent
in { value: SyntaxTree::Const } |
{ value: SyntaxTree::Kw[value: "self"] }
nil
in receiver: { value: { value: framework } }
framework
in value: { value: framework }
framework
end

return if current_framework == framework

raise "Expected #{current_framework} to match #{framework}"
target = "#{prefix}.#{name}"

string_value = case value
in Prism::ConstantPathNode
value.full_name
in Prism::HashNode
HashToString.new.tap { |v| v.visit(value) }.to_s
in Prism::InterpolatedStringNode
"\"#{value.parts.map(&:content).join("")}\""
in Prism::FalseNode
"false"
in Prism::TrueNode
"true"
else
value.location.slice
end

@configs[target] = string_value
end

private
def current_framework
@framework_stack.last
end

def respond_to_framework?(node)
if node in SyntaxTree::CallNode[
receiver: nil,
message: SyntaxTree::Ident[value: "respond_to?"],
arguments: SyntaxTree::ArgParen[
arguments: SyntaxTree::Args[
parts: [
SyntaxTree::SymbolLiteral[
value: SyntaxTree::Ident[value: new_framework]
]
]
]
]
]
if node in Prism::CallNode[
name: :respond_to?,
arguments: Prism::ArgumentsNode[
arguments: [
Prism::SymbolNode[unescaped: new_framework]
]
]
]
new_framework
end
end
Expand Down

This file was deleted.

2 changes: 1 addition & 1 deletion tools/rail_inspector/rail_inspector.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@ Gem::Specification.new do |spec|
spec.require_paths = ["lib"]

# Uncomment to register a new dependency of your gem
spec.add_dependency "syntax_tree", "6.1.1"
spec.add_dependency "prism", "~> 1.2"
spec.add_dependency "thor", "~> 1.0"
end
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# frozen_string_literal: true

require "test_helper"
require "rail_inspector/visitor/framework_default"

class FrameworkDefaultTest < Minitest::Test
Expand Down Expand Up @@ -81,6 +82,37 @@ def test_nested_frameworks_raise_when_strict
ENV["STRICT"] = original_env
end

def test_multiline_strings
config = config_for_defaults <<~RUBY
case target_version.to_s
when "7.0"
if respond_to?(:active_storage)
active_storage.video_preview_arguments =
"-vf 'select=eq(n\\,0)+eq(key\\,1)+gt(scene\\,0.015),loop=loop=-1:size=2,trim=start_frame=1'" \
" -frames:v 1 -f image2"
end
end
RUBY

assert_includes config, "7.0"
assert_equal(
"\"-vf 'select=eq(n\\,0)+eq(key\\,1)+gt(scene\\,0.015),loop=loop=-1:size=2,trim=start_frame=1' -frames:v 1 -f image2\"",
config["7.0"]["active_storage.video_preview_arguments"],
)
end

def test_inline_condition
config = config_for_defaults <<~RUBY
case target_version.to_s
when "8.0"
Regexp.timeout ||= 1 if Regexp.respond_to?(:timeout=)
end
RUBY

assert_includes config, "8.0"
assert_equal("1", config["8.0"]["Regexp.timeout"])
end

private
def wrapped_defaults(defaults)
<<~RUBY
Expand All @@ -94,7 +126,7 @@ def load_defaults(target_version)

def config_for_defaults(defaults)
full_class = wrapped_defaults(defaults)
parsed = SyntaxTree.parse(full_class)
parsed = Prism.parse(full_class).value
visitor.visit(parsed)
visitor.config_map
end
Expand Down

0 comments on commit 5a9ef5c

Please sign in to comment.