Skip to content

Commit 331e13a

Browse files
authored
Merge pull request #4 from exercism/exploring-3
First approach restructured
2 parents eb11ee1 + 0fb627d commit 331e13a

11 files changed

+535
-95
lines changed

lib/analyzers/exercise_analyzer.rb

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
class ExerciseAnalyzer
2+
3+
# This is just flow-control for quickly exiting the
4+
# analysis. We probably don't want to do things this
5+
# way eventually, but it helps remove noise for now.
6+
class FinishedFlowControlException < RuntimeError
7+
end
8+
29
def initialize(code_to_analyze)
310
@code_to_analyze = code_to_analyze
411
@approve = false
@@ -7,11 +14,13 @@ def initialize(code_to_analyze)
714
end
815

916
def call
10-
analyze!
17+
begin
18+
analyze!
19+
rescue FinishedFlowControlException
20+
end
1121
results
1222
end
1323

14-
1524
def results
1625
{
1726
approve: approve,

lib/analyzers/two_fer/analyze.rb

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,275 @@
11
module TwoFer
2+
3+
MESSAGES = {
4+
no_module: "No module or class called TwoFer",
5+
no_method: "No method called two_fer",
6+
splat_args: "Rather than using *%s, how about acutally setting a paramater called 'name'?",
7+
missing_default_param: "There is not a correct default param - the tests will fail",
8+
incorrect_default_param: "You could set the default value to 'you' to avoid conditionals",
9+
string_building: "Rather than using string building, use interpolation",
10+
kernel_format: "Rather than using the format method, use interpolation",
11+
string_format: "Rather than using string's format/percentage method, use interpolation"
12+
}
13+
214
class Analyze < ExerciseAnalyzer
315
include Mandate
416

517
def analyze!
18+
# Note that all "check_...!" methods exit this method if the solution
19+
# is approved or disapproved, so each step is only called if the
20+
# previous one has not resolved what to do.
21+
22+
# Firstly we want to check that the structure of this
23+
# solution is correct and that there is nothing structural
24+
# stopping it from passing the tests
25+
check_structure!
26+
27+
# Now we want to ensure that the method signature
28+
# is sane and that it has valid arguments
29+
check_method_signature!
30+
31+
# There is one optimal solution for two-fer which needs
32+
# no commnents and can just be approved. If we have it, then
33+
# let's just acknowledge it and get out of here.
34+
check_for_optimal_solution!
35+
36+
# We often see solutions that are correct but use different
37+
# string concatenation options (e.g. string#+, String.format, etc)
38+
# We'll approve these but want to leave a comment that introduces
39+
# them to string interpolation in case they don't know about it.
40+
check_for_correct_solution_without_string_interpolaton!
41+
42+
# The most common error in twofer is people using conditionals
43+
# to check where the value passed in is nil, rather than using a defaul
44+
# value. We want to check for conditionals and tell the user about the
45+
# default paramater if we see one.
46+
check_for_conditional_on_default_argument!
47+
48+
# Sometiems people specify the names (if name == "Alice" ...). If we
49+
# do this, suggest using string interpolation to make us of the
50+
# paramter, rather than using a conditional on it.
51+
# check_for_names!
52+
53+
# We don't have any idea about this solution, so let's refer it to a
54+
# mentor and get exit our analysis.
55+
refer_to_mentor!
56+
end
57+
58+
# ###
59+
# Analysis functions
60+
# ###
61+
def check_structure!
62+
# First we check that there is a two-fer class or module
63+
# and that it contains a method called two-fer
64+
disapprove!(:no_module) unless two_fer_module
65+
disapprove!(:no_method) unless main_method
66+
end
67+
68+
def check_method_signature!
69+
# If there is no parameter or it doesn't have a default value,
70+
# then this solution won't pass the tests.
71+
disapprove!(:missing_default_param) if paramaters.size != 1
72+
73+
# If they provide a splat, the tests can pass but we
74+
# should suggest they use a real paramater
75+
disapprove!(:splat_args, first_paramater_name) if first_paramater.restarg_type?
76+
77+
# If they don't provide an optional argument the tests will fail
78+
disapprove!(:missing_default_param) unless first_paramater.optarg_type?
79+
end
80+
81+
def check_for_optimal_solution!
82+
# The optional solution looks like this:
83+
#
84+
# def self.two_fer(name="you")
85+
# "One for #{name}, one for me."
86+
# end
87+
88+
# The default argument must be 'you', and it must just be a single
89+
# statement using interpolation. Other solutions might be approved
90+
# but this is the only one that we would approve without comment.
91+
92+
return unless default_argument_is_optimal?
93+
return unless one_line_solution?
94+
return unless using_string_interpolation?
95+
96+
# If the interpolation has more than three components, then they've
97+
# done something weird, so let's get a mentor to look at it!
98+
refer_to_mentor! unless string_interpolation_has_three_components?
99+
100+
approve!
101+
end
102+
103+
def check_for_correct_solution_without_string_interpolaton!
104+
# If we don't have a correct default argugment or a one line
105+
# solution then let's just get out of here.
106+
return unless default_argument_is_optimal?
107+
return unless one_line_solution?
108+
109+
loc = first_line_in_method(main_method)
110+
111+
# In the case of:
112+
# "One for " + name + ", one for me."
113+
if loc.method_name == :+ &&
114+
loc.arguments[0].type == :str
115+
approve!(:string_building)
116+
end
117+
118+
# In the case of:
119+
# format("One for %s, one for me.", name)
120+
if loc.method_name == :format &&
121+
loc.receiver == nil &&
122+
loc.arguments[0].type == :str &&
123+
loc.arguments[1].type == :lvar
124+
approve!(:kernel_format)
125+
end
126+
127+
# In the case of:
128+
# "One for %s, one for me." % name
129+
if loc.method_name == :% &&
130+
loc.receiver.type == :str &&
131+
loc.arguments[0].type == :lvar
132+
approve!(:string_format)
133+
end
134+
135+
# If we have a one-line method that passes the tests, then it's not
136+
# soemthing we've planned for, so let's refer it to a mentor
137+
return refer_to_mentor!
138+
end
139+
140+
def check_for_conditional_on_default_argument!
141+
loc = first_line_in_method(main_method)
142+
143+
# If we don't have a conditional, then let's get out of here.
144+
#
145+
# TODO: We might wnt to refactor this to extract a conditional from the
146+
# method rather than insist on it being the first line.
147+
return unless loc.type == :if
148+
149+
# Get the clause of the conditional (ie the bit after the "if" keyword)
150+
conditional = extract_conditional_clause(loc)
151+
152+
# Let's warn about using a better default if they `if name == nil`
153+
if is_lvar?(conditional.receiver, :name) &&
154+
conditional.first_argument == default_argument
155+
disapprove!(:incorrect_default_param)
156+
end
157+
158+
# Same thing but if they do it the other way round, ie `if nil == name`
159+
if conditional.receiver == default_argument &&
160+
is_lvar?(conditional.first_argument, :name)
161+
disapprove!(:incorrect_default_param)
162+
end
163+
end
164+
165+
# ###
166+
# Analysis helpers
167+
# ###
168+
def default_argument_is_optimal?
169+
default_argument_value == "you"
170+
end
171+
172+
def one_line_solution?
173+
main_method.body.line_count == 1
174+
end
175+
176+
def using_string_interpolation?
177+
main_method.body.dstr_type?
178+
end
179+
180+
def string_interpolation_has_three_components?
181+
#main_method.body.pry
182+
main_method.body.children.size == 3
183+
end
184+
185+
# ###
186+
# Static analysis helpers
187+
# ###
188+
def num_lines_in_method(method)
189+
method.body.child_nodes.size
190+
end
191+
192+
def first_line_in_method(method)
193+
# A begin block signifies multiple lines
194+
# so we return the first line.
195+
method.body.children.first if main_method.body.type == :begin
196+
197+
# Without a begin block we just have one line,
198+
# so we return the method body, which *is* the first line
199+
method.body
200+
end
201+
202+
# Is this an lvar (local variable) with a given name?
203+
def is_lvar?(node, name)
204+
node.lvar_type? && node.children[0] == name
205+
end
206+
207+
def extract_conditional_clause(loc)
208+
loc.children[0]
209+
end
210+
211+
memoize
212+
def two_fer_module
213+
ExtractModuleOrClass.(root_node, "TwoFer")
214+
end
215+
216+
memoize
217+
def main_method
218+
ExtractClassMethod.(two_fer_module, "two_fer")
219+
end
220+
221+
memoize
222+
def paramaters
223+
main_method.arguments
224+
end
225+
226+
memoize
227+
def first_paramater
228+
paramaters.first
229+
end
230+
231+
def first_paramater_name
232+
first_paramater.children[0]
233+
end
234+
235+
def default_argument
236+
first_paramater.children[1]
237+
end
238+
239+
def default_argument_value
240+
default_argument.children[0]
241+
end
242+
243+
244+
# ###
245+
# Flow helpers
246+
#
247+
# These are totally generic to all exercises and
248+
# can probably be extracted to parent
249+
# ###
250+
def approve!(msg = nil)
251+
self.messages << MESSAGES[msg] if msg
252+
self.approve = true
253+
254+
raise FinishedFlowControlException
255+
end
256+
257+
def refer_to_mentor!
258+
self.refer_to_mentor = true
259+
260+
# Refering to mentor is an explicit
261+
# command resulting from weirdness
262+
# and should override approving
263+
self.approve = false
264+
265+
raise FinishedFlowControlException
266+
end
267+
268+
def disapprove!(msg, *msg_args)
269+
self.messages << (MESSAGES[msg] % msg_args)
270+
self.approve = false
271+
272+
raise FinishedFlowControlException
6273
end
7274
end
8275
end

lib/generic/extract_class_method.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
class ExtractClassMethod < Parser::AST::Processor
2+
include Mandate
3+
4+
def initialize(node_to_search, name)
5+
@node_to_search = node_to_search
6+
@name = name.to_sym
7+
@found_method = nil
8+
end
9+
10+
def call
11+
process(node_to_search)
12+
found_method
13+
end
14+
15+
def on_sclass(node)
16+
# If we're in a class << self context
17+
if node.children[0].self_type?
18+
@found_method = ExtractInstanceMethod.(node.children[1], name)
19+
end
20+
end
21+
22+
def on_defs(node)
23+
if node.method_name == name
24+
@found_method = node
25+
end
26+
end
27+
28+
private
29+
attr_reader :node_to_search, :name, :found_method
30+
end
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
class ExtractInstanceMethod < Parser::AST::Processor
2+
include Mandate
3+
4+
def initialize(node_to_search, name)
5+
@node_to_search = node_to_search
6+
@name = name.to_sym
7+
@found_method = nil
8+
end
9+
10+
def call
11+
process(node_to_search)
12+
found_method
13+
end
14+
15+
def on_def(node)
16+
if node.method_name == name
17+
@found_method = node
18+
end
19+
end
20+
21+
private
22+
attr_reader :node_to_search, :name, :found_method
23+
end
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
class ExtractModuleOrClass < Parser::AST::Processor
2+
include Mandate
3+
4+
def initialize(node_to_search, name)
5+
@node_to_search = node_to_search
6+
@name = name
7+
@found_constant = nil
8+
end
9+
10+
def call
11+
process(node_to_search)
12+
found_constant
13+
end
14+
15+
def on_module(node)
16+
inspect_node(node)
17+
end
18+
19+
def on_class(node)
20+
inspect_node(node)
21+
end
22+
23+
private
24+
attr_reader :node_to_search, :name, :found_constant
25+
26+
def inspect_node(node)
27+
if node.children.first.const_name == name
28+
@found_constant = node
29+
end
30+
end
31+
end
32+

0 commit comments

Comments
 (0)