Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Style/LineBreak Cops #2277

Merged
merged 1 commit into from
Oct 16, 2015
Merged

Conversation

panthomakos
Copy link
Contributor

When large multi-line lists are present in a code base it's often
rather arbitrary to align them based on the first line/element:

long_variable = { foo: 'foo',
                  bar: 'bar',
                  baz: 'baz' }

In this example if the variable name is refactored the entire list
formatting also needs to change. This makes diffs, in particular, very
hard to read due to the large number of formatting-only changes:

short = { foo: 'foo',
          bar: 'bar',
          baz: 'baz' }

These alignments also greatly reduce the available space on a line for
each element, especially if there is a maximum line length enforcement.

The Style/MultilineList cop along with any appropriate Align or Indent
cop (for example HashAlign and HashIndent) allows you to enforce a
more diff-friendly and short-line style:

variable = {
  foo: 'foo',
  bar: 'bar',
  baz: 'baz' }

Mutually-exclusive configuration options are provided for each of method
parameters, method arguments, array elements and hash elements. Care has
been taken to ensure that DSL-like method calls are ignored and do not
result in invalid Ruby.

I have added tests for all use-cases I could find in some large Ruby and
Rails code-bases.

class MultilineList < Cop
MSG = 'Move all elements below the first line of a multi-line list.'

def on_def(node)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to handle defs nodes too, and you can do this by using the OnMethodDef mixin.

@panthomakos
Copy link
Contributor Author

jonas054 Thank you very much for the review. I've addressed all of your comments and amended my commit. The autocorrect method is also a lot simpler because I am using the first element as the offense (instead of the parent node) which means inserting a newline is much simpler. PTAL

@@ -525,6 +525,13 @@ Style/MethodName:
- snake_case
- camelCase

Style/MultilineList:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cop is enabled by default but each part is disabled by default. I think it would be better to do it the other way around, because then people can find it when they look in disabled.yml for extra things to turn on.

@jonas054
Copy link
Collaborator

That's much better. I've added some more comments, mostly because I didn't get around to do it earlier.

@panthomakos
Copy link
Contributor Author

@jonas054 I've addressed your comments. Thanks again for the review. I'm learning a lot about Ruby code parsing :)

PTAL

@panthomakos
Copy link
Contributor Author

@jonas054 I am taking a look at some issues w/ the array assignment part. After making your changes I am getting some infinite loops for some implicit arrays in setters. I am investigating to see if I can write a simple test case and resolve the issue.


def assignment_on_same_line?(node)
source = node.loc.expression.source_line[0...node.loc.column]
source =~ /\s*\=\s*$/
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jonas054 I was unable to convert this using the lhs, rhs code you suggested because it does not work for some "assignment" types. It does work for masgn but not for a send type. For example:

config.values =
  :a,
  :b,
  :c

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Well, both assignment_on_same_line? and method_uses_parens? handle more than one node type, and then you have to do it with regexp matching or add handling of the different cases. I guess it's OK as it is.

@panthomakos
Copy link
Contributor Author

@jonas054 I resolved the issue w/ implicit arrays and added a test case to demonstrate the issue. PTAL

return if children.size < 2

line = start.loc.line
min = children.min_by { |n| n.loc.line }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jonas054 I have a question related to this code. Is there a standard way you check for "does this node span multiple lines"? For example, the following test case won't pass this implementation:

something('foo', bar: 1,
  baz: 2)

The reason is that even though there are two children, they both start on the same line. While I could make some kind of check to see if any of the individual children have a "\n" in their source, it seems like there might be a better way to address this issue. Do you know if there is some way I can go about check this so that even these cases are tagged as offenses?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming that the node has a begin and an end, which I think it will in this case, you can use node.loc.begin.line and node.loc.end.line. The same will hold true for other attributes off of loc.

@panthomakos
Copy link
Contributor Author

@rrosenblum I've incorporated your comments. I resolved the issue with elements spanning multiple lines by using the first_line and last_line of the loc object instead of just the line number. I've added a test case that passes with the updated code as well.

check_children(node, node.children)
elsif method_uses_parens?(node.parent, node)
check_children(node, node.children, node.parent)
else
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

else return isn't really needed.

@panthomakos
Copy link
Contributor Author

Incorporated comments. Removed the else return.

@jonas054
Copy link
Collaborator

jonas054 commented Oct 1, 2015

👍 Looks good!

@panthomakos
Copy link
Contributor Author

ping @jonas054 @rrosenblum I rebased off master again. Is there anything else I need to do here to get this change merged to master?

@rrosenblum
Copy link
Contributor

@panthomakos the build is failing for an incorrect format in the CHANGELOG. There is a missing colon after the link to the PR.

Should be:

* [#2277](https://github.com/bbatsov/rubocop/pull/2277):

@panthomakos
Copy link
Contributor Author

@rrosenblum fixed the CHANGELOG formatting

it 'autocorrects the offense' do
new_source = autocorrect_source(cop, source)

expect(new_source).to eq(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem like the correct auto-correction to me. The example given in the documentation comment shows

# good with EnableMethodParams
def method(
  foo, bar,
  baz)
  do_something
end

The auto-correct places the first element left justified with the def.

def foo(
bar,
  baz)
  do_something
end

Should it be indented or is it relying on another cop to handle that indention?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any additional alignment should then be fixed by Style/AlignParameters:

http://www.rubydoc.info/gems/rubocop/0.29.1/RuboCop/Cop/Style/AlignParameters

@bbatsov
Copy link
Collaborator

bbatsov commented Oct 13, 2015

I haven't had time to review the code, but I can tell you that I definitely don't like the name of the cop, as it's not clear what it refers to. Certainly, the term "list" is not used often in Ruby.

@panthomakos
Copy link
Contributor Author

@bbatsov I suppose the other option here is to have 4 separate cops that indicate that a line break is necessary, something like: FirstHashElementLinePlacement or FirstArrayElementLineBreak. Maybe I can generalize it to FirstMultilineElementLinePlacement?

@bbatsov
Copy link
Collaborator

bbatsov commented Oct 15, 2015

Probably this should be a few separate cops. @jonas054 Thoughts?

@jonas054
Copy link
Collaborator

I hadn't thought about that while reviewing, but sure, why not? It's more simple in a way to have four separate cops than one cop with four enable properties. And the naming should be more natural.

@panthomakos panthomakos changed the title Add Style/MultilineList Cop. Add Style/LineBreak Cops. Oct 15, 2015
@panthomakos
Copy link
Contributor Author

@bbatsov @jonas054

Please take another look. I've split the cop up into 4 separate cops with cleaner descriptions and messages.

@jonas054
Copy link
Collaborator

👍 Looks good to me.

@bbatsov
Copy link
Collaborator

bbatsov commented Oct 16, 2015

Looks good. Just remove the . in the commit message's title.

Btw, there should probably a set of complementary checks for the final parenthesis, so you can enforce either this style:

variable = {
  foo: 'foo',
  bar: 'bar',
  baz: 'baz' }

or this one (which is my preference):

variable = {
  foo: 'foo',
  bar: 'bar',
  baz: 'baz' 
}

* `Style/FirstArrayElementLineBreak` checks for a line break before the
  first element in a multi-line array.
* `Style/FirstHashElementLineBreak` checks for a line break before the
  first element in multi-line hash.
* `Style/FirstMethodArgumentLineBreak` checks for a line break before
  the first argument in a multi-line method call.
* `Style/FirstMethodParameterLineBreak` checks for a line break before
  the first parameter in a multi-line method parameter definition.

When large multi-line lists are present in a code base it's often
rather arbitrary to align them based on the first element:

```
long_variable = { foo: 'foo',
                  bar: 'bar',
                  baz: 'baz' }
```

In this example if the variable name is refactored the entire list
formatting also needs to change. This makes diffs, in particular, very
hard to read due to the large number of formatting-only changes:

```
short = { foo: 'foo',
          bar: 'bar',
          baz: 'baz' }
```

These alignments also greatly reduce the available space on a line for
each element, especially if there is a maximum line length enforcement.

This collection of cops along with any appropriate Align or Indent
cop (for example `HashAlign` and `HashIndent`) allows you to enforce a
more diff-friendly and short-line style:

```
variable = {
  foo: 'foo',
  bar: 'bar',
  baz: 'baz' }
```

Care has been taken to ensure that DSL-like method calls are ignored and
do not result in invalid Ruby. I have also added tests for all use-cases
I could find in some large Ruby and Rails code-bases.
@panthomakos panthomakos changed the title Add Style/LineBreak Cops. Add Style/LineBreak Cops Oct 16, 2015
@panthomakos
Copy link
Contributor Author

@bbatsov Removed period from commit message.

I prefer the second as well. I was thinking about adding that set of cops after this PR was complete.

bbatsov added a commit that referenced this pull request Oct 16, 2015
@bbatsov bbatsov merged commit 5c9fa95 into rubocop:master Oct 16, 2015
@bbatsov
Copy link
Collaborator

bbatsov commented Oct 16, 2015

I was thinking about adding that set of cops after this PR was complete.

Looking forward to those. :-)

panthomakos added a commit to panthomakos/rubocop that referenced this pull request Dec 27, 2015
This cop is inspired by a comment in the `Style/LineBreaks` cop PR:

rubocop#2277 (comment)

This cop checks that the closing brace in an array literal is symmetrical
with respect to the opening brace and the array elements. I am very open
to suggestions on naming and ways to clarify the offense messages.

    # bad
    [ :a,
      :b
    ]

    # bad
    [
      :a,
      :b ]

    # good
    [ :a,
      :b ]

    #good
    [
      :a,
      :b
    ]

My intention is to expand this to account for hashes, method parameters
and method arguments once the language and approach is validated.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants