near-miss-clone

Blocks of code are alike but for a few names or literals.

Group
Opportunities
Runs
Runs under ur report: ur report --rule near-miss-clone
Level
note
Reads
Every file
Fix
None: the shared function's name, parameters, and home are a design choice

Why

These runs of statements, or match arms, are the same once a few names, fields, or literals are holes, and the finding lists the values each hole takes. A name each block binds for itself is a renaming, not a hole. One function that takes the holes as parameters, or a table of their values, would replace them. Only classes of at most three holes are reported. It reads every language whose grammar states its shapes in a Shapes.rsc; an advisory rule that ur report runs.

The fix, as Ur suggests it: write one function that takes the values that differ as parameters, or a table of them, and use it at each site.

For agents

Before copying a block and changing a few names or literals, make the block a function of those values, or a loop over a table of them.

Example

accounts/forms.py
def validate_signup(form):
    errors = {}

    value = form.data.get("email", "").strip()
    if not value:
        errors["email"] = "This field is required."
    elif len(value) > 254:
        errors["email"] = "Email addresses are at most 254 characters."

    value = form.data.get("username", "").strip()
    if not value:
        errors["username"] = "This field is required."
    elif len(value) > 32:
        errors["username"] = "Usernames are at most 32 characters."

    value = form.data.get("display_name", "").strip()
    if not value:
        errors["display_name"] = "This field is required."
    elif len(value) > 64:
        errors["display_name"] = "Display names are at most 64 characters."

    return errors
What Ur prints
$ ur report --rule near-miss-clone
opportunity[near-miss-clone]: 3 runs of 2 statements are alike but for 3 values: `"display_name"` / `"email"` / `"username"`; `254` / `32` / `64`; `"Display names are at most 64 characters."` / `"Email addresses are at most 254 characters."` / `"Usernames are at most 32 characters."` (saves about 6 lines)
  --> ./accounts/forms.py:4:5-8 in `validate_signup`
  --> ./accounts/forms.py:10:5-14 in `validate_signup`
  --> ./accounts/forms.py:16:5-20 in `validate_signup`
  = hint: write one function that takes the values that differ as parameters, or a table of them, and use it at each site
1 opportunity saving about 6 lines in all

Try it in the playground →