|
1 | 1 | 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 | + |
2 | 14 | class Analyze < ExerciseAnalyzer
|
3 | 15 | include Mandate
|
4 | 16 |
|
5 | 17 | 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 |
6 | 273 | end
|
7 | 274 | end
|
8 | 275 | end
|
0 commit comments