Skip to content

Commit 8491ae4

Browse files
committed
Add node extraction and extra two-fer tests
1 parent dc1c6c4 commit 8491ae4

File tree

11 files changed

+242
-83
lines changed

11 files changed

+242
-83
lines changed

lib/analyzer.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
require_relative "generic/extract_module_method"
1717
require_relative "generic/extract_instance_method"
1818
require_relative "generic/extract_module_or_class"
19+
require_relative "generic/extract_nodes"
1920

2021
require_relative 'analyze_solution'
2122

lib/analyzers/exercise_analyzer.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
class ExerciseAnalyzer
2+
include SA::InlineHelpers
23

34
# This is just flow-control for quickly exiting the
45
# analysis. We probably don't want to do things this

lib/analyzers/two_fer/analyze.rb

Lines changed: 83 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ module TwoFer
44
no_module: "ruby.general.no_target_module",
55
no_method: "ruby.general.no_target_method",
66
incorrect_indentation: "ruby.general.incorrect_indentation",
7+
explicit_return: "ruby.general.explicit_return", #"The last line automatically gets returned"
78
splat_args: "ruby.two_fer.splat_args", #Rather than using *%s, how about actually setting a parameter called 'name'?",
89
missing_default_param: "ruby.two_fer.missing_default_param", #"There is no correct default param - the tests will fail",
910
incorrect_default_param: "ruby.two_fer.incorrect_default_param", #You could set the default value to 'you' to avoid conditionals",
11+
reassigning_param: "ruby.two_fer.reassigning_param", # You don't need to reassign - use the default param
1012
string_building: "ruby.two_fer.avoid_string_building", # "Rather than using string building, use interpolation",
1113
kernel_format: "ruby.two_fer.avoid_kernel_format", #"Rather than using the format method, use interpolation",
1214
string_format: "ruby.two_fer.avoid_string_format", #"Rather than using string's format/percentage method, use interpolation"
@@ -16,6 +18,8 @@ class Analyze < ExerciseAnalyzer
1618
include Mandate
1719

1820
def analyze!
21+
#target_method.pry
22+
1923
# Note that all "check_...!" methods exit this method if the solution
2024
# is approved or disapproved, so each step is only called if the
2125
# previous one has not resolved what to do.
@@ -46,6 +50,10 @@ def analyze!
4650
# default parameter if we see one.
4751
check_for_conditional_on_default_argument!
4852

53+
# Sometimes, rather than setting a variable, people reassign the input param e.g.
54+
# use name ||= "you"
55+
check_for_reassigned_parameter!
56+
4957
# Sometimes people specify the names (if name == "Alice" ...). If we
5058
# do this, suggest using string interpolation to make us of the
5159
# parameter, rather than using a conditional on it.
@@ -90,13 +98,33 @@ def check_for_optimal_solution!
9098
# statement using interpolation. Other solutions might be approved
9199
# but this is the only one that we would approve without comment.
92100

101+
93102
return unless default_argument_is_optimal?
94103
return unless one_line_solution?
95-
return unless using_string_interpolation?
96104

97-
# If the interpolation has more than three components, then they've
105+
if target_method.body.return_type?
106+
loc = target_method.body.children.first
107+
explicit_return = true
108+
elsif target_method.body.dstr_type?
109+
loc = target_method.body
110+
explicit_return = false
111+
else
112+
# We're only expecting one of these two scenarios
113+
return
114+
end
115+
116+
# Return unless we have string interpolation
117+
return unless loc.dstr_type?
118+
119+
# If the interpolation does not follow this pattern then the student has
98120
# done something weird, so let's get a mentor to look at it!
99-
refer_to_mentor! unless string_interpolation_has_three_components?
121+
refer_to_mentor! unless loc.children[0] == s(:str, "One for ") &&
122+
loc.children[1] == s(:begin, s(:lvar, first_parameter_name)) &&
123+
loc.children[2] == s(:str, ", one for me.")
124+
125+
# If we're got a correct solution but they've given an explicit
126+
# return then let's warn them against that.
127+
disapprove!(:explicit_return) if explicit_return
100128

101129
approve_if_whitespace_is_sensible!
102130
end
@@ -109,6 +137,9 @@ def check_for_correct_solution_without_string_interpolaton!
109137

110138
loc = SA::Helpers.extract_first_line_from_method(target_method)
111139

140+
# Protect against explicit return
141+
#loc = loc.children.first if loc.return_type?
142+
112143
# In the case of:
113144
# "One for " + name + ", one for me."
114145
if loc.method_name == :+ &&
@@ -139,28 +170,65 @@ def check_for_correct_solution_without_string_interpolaton!
139170
end
140171

141172
def check_for_conditional_on_default_argument!
142-
loc = SA::Helpers.extract_first_line_from_method(target_method)
173+
stmts = SA::Helpers.extract_nodes(:if, target_method)
143174

144-
# If we don't have a conditional, then let's get out of here.
145-
#
146-
# TODO: We might want to refactor this to extract a conditional from the
147-
# method rather than insist on it being the first line.
148-
return unless loc.type == :if
175+
# If there are no statements then we can safely get out of here.
176+
return if stmts.empty?
177+
178+
# If there is more than one statement, then let's refer this to a mentor
179+
refer_to_mentor! if stmts.size > 1
149180

150-
# Get the clause of the conditional (i.e. the bit after the "if" keyword)
151-
conditional = SA::Helpers.extract_conditional_clause(loc)
181+
# Now we have the statement, let's also get the conditional
182+
# clause (i.e. the bit after the "if" keyword)
183+
if_statement = stmts.first
184+
conditional = SA::Helpers.extract_conditional_clause(if_statement)
185+
186+
if SA::Helpers.lvar?(conditional, first_parameter_name)
187+
# Let's warn about using a better default if they do `if name`
188+
disapprove!(:incorrect_default_param)
189+
190+
elsif conditional.send_type? &&
191+
SA::Helpers.lvar?(conditional.receiver, first_parameter_name)
192+
conditional.first_argument == :nil?
193+
# Let's warn about using a better default if they do `if name.nil?`
194+
disapprove!(:incorrect_default_param)
152195

153196
# Let's warn about using a better default if they `if name == nil`
154-
if SA::Helpers.lvar?(conditional.receiver, :name) &&
197+
elsif SA::Helpers.lvar?(conditional.receiver, first_parameter_name) &&
155198
conditional.first_argument == default_argument
156199
disapprove!(:incorrect_default_param)
157-
end
158200

159201
# Same thing but if they do it the other way round, i.e. `if nil == name`
160-
if conditional.receiver == default_argument &&
161-
SA::Helpers.lvar?(conditional.first_argument, :name)
202+
elsif SA::Helpers.lvar?(conditional.first_argument, first_parameter_name) &&
203+
conditional.receiver == default_argument
162204
disapprove!(:incorrect_default_param)
163205
end
206+
207+
# If we have an if without that does not do an expected comparison,
208+
# let's refer this to a mentor and get out of here!
209+
refer_to_mentor!
210+
end
211+
212+
def check_for_reassigned_parameter!
213+
assignments = SA::Helpers.extract_nodes(:or_asgn, target_method)
214+
215+
# If there are no statements then we can safely get out of here.
216+
return if assignments.empty?
217+
218+
# If there is more than one statement, then let's refer this to a mentor
219+
refer_to_mentor! if assignments.size > 1
220+
221+
# Now we what is being reassigned is the param and it's being rassigned to "you"
222+
# let's warn the user
223+
assignment = assignments.first
224+
if assignment.children[0] == s(:lvasgn, first_parameter_name) &&
225+
assignment.children[1] == s(:str, "you")
226+
disapprove!(:reassigning_param)
227+
end
228+
229+
# If we have a reassignment that doesn't conform to this
230+
# let's refer this to a mentor and get out of here!
231+
refer_to_mentor!
164232
end
165233

166234
# ###
@@ -174,15 +242,6 @@ def one_line_solution?
174242
target_method.body.line_count == 1
175243
end
176244

177-
def using_string_interpolation?
178-
target_method.body.dstr_type?
179-
end
180-
181-
def string_interpolation_has_three_components?
182-
#target_method.body.pry
183-
target_method.body.children.size == 3
184-
end
185-
186245
# REFACTOR: This could be refactored to strip blank
187246
# lines and then use each_cons(2).
188247
def indentation_is_sensible?

lib/generic/extract_nodes.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
module SA
2+
class ExtractNodes < Parser::AST::Processor
3+
include Mandate
4+
5+
def initialize(node_type, node_to_search)
6+
@node_to_search = node_to_search
7+
@found_methods = []
8+
9+
define_singleton_method "on_#{node_type}" do |node|
10+
found_methods << node
11+
end
12+
end
13+
14+
def call
15+
process(node_to_search)
16+
found_methods
17+
end
18+
19+
private
20+
attr_reader :node_to_search, :found_methods
21+
end
22+
end

lib/generic/helpers.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
module SA
2+
module InlineHelpers
3+
def s(type, *children)
4+
Parser::AST::Node.new(type, children)
5+
end
6+
end
7+
28
module Helpers
39
def self.extract_module_or_class(*args)
410
ExtractModuleOrClass.(*args)
@@ -8,6 +14,10 @@ def self.extract_module_method(*args)
814
ExtractModuleMethod.(*args)
915
end
1016

17+
def self.extract_nodes(*args)
18+
ExtractNodes.(*args)
19+
end
20+
1121
def self.extract_first_line_from_method(method)
1222
# A begin block signifies multiple lines
1323
# so we return the first line.

manual_test.rb

Lines changed: 0 additions & 20 deletions
This file was deleted.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
2+
require "analyzer"
3+
4+
output = []
5+
["two-fer"].each do |slug|
6+
path = File.expand_path("#{__FILE__}/../../test/fixtures/#{slug}")
7+
statuses = Hash.new {|h,k|h[k] = 0}
8+
Dir.foreach(path) do |id|
9+
next if id == "." || id == ".."
10+
11+
begin
12+
analysis = TwoFer::Analyze.(File.read("#{path}/#{id}/two_fer.rb"))
13+
p id and exit if analysis[:status] == :refer_to_mentor
14+
rescue
15+
p id and exit
16+
statuses["exploded"] += 1
17+
end
18+
end
19+
end

scripts/manual_test.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
2+
require "analyzer"
3+
require 'pp'
4+
require 'pry'
5+
6+
source = %q{
7+
class TwoFer
8+
def self.two_fer(name = "you")
9+
return ("One for " + name + ", one for me.")
10+
end
11+
end
12+
13+
}
14+
15+
puts "\n\n\n\n"
16+
pp TwoFer::Analyze.(source)
17+
puts "\n\n\n\n"

0 commit comments

Comments
 (0)