tailor 1.3.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -0
  3. data/Gemfile.lock +27 -25
  4. data/History.md +36 -0
  5. data/README.md +10 -4
  6. data/Rakefile +6 -6
  7. data/features/support/env.rb +2 -2
  8. data/features/support/legacy/bad_ternary_colon_spacing.rb +1 -1
  9. data/features/support/legacy/long_file_with_indentation.rb +2 -2
  10. data/features/support/world.rb +1 -1
  11. data/features/valid_ruby.feature +1 -1
  12. data/lib/tailor/cli/options.rb +19 -1
  13. data/lib/tailor/configuration.rb +2 -2
  14. data/lib/tailor/configuration/style.rb +6 -0
  15. data/lib/tailor/lexed_line.rb +2 -2
  16. data/lib/tailor/lexer/lexer_constants.rb +1 -0
  17. data/lib/tailor/lexer/token.rb +2 -2
  18. data/lib/tailor/logger.rb +2 -2
  19. data/lib/tailor/rake_task.rb +2 -2
  20. data/lib/tailor/ruler.rb +0 -1
  21. data/lib/tailor/rulers/allow_camel_case_methods_ruler.rb +0 -1
  22. data/lib/tailor/rulers/allow_conditional_parentheses.rb +65 -0
  23. data/lib/tailor/rulers/allow_unnecessary_double_quotes_ruler.rb +61 -0
  24. data/lib/tailor/rulers/allow_unnecessary_interpolation_ruler.rb +87 -0
  25. data/lib/tailor/rulers/indentation_spaces_ruler.rb +40 -50
  26. data/lib/tailor/rulers/indentation_spaces_ruler/argument_alignment.rb +63 -0
  27. data/lib/tailor/rulers/indentation_spaces_ruler/ast_xml.rb +89 -0
  28. data/lib/tailor/rulers/indentation_spaces_ruler/indentation_manager.rb +2 -14
  29. data/lib/tailor/rulers/indentation_spaces_ruler/line_continuations.rb +58 -0
  30. data/lib/tailor/rulers/max_code_lines_in_class_ruler.rb +3 -3
  31. data/lib/tailor/rulers/max_code_lines_in_method_ruler.rb +3 -3
  32. data/lib/tailor/rulers/spaces_after_comma_ruler.rb +3 -3
  33. data/lib/tailor/rulers/spaces_after_conditional_ruler.rb +3 -3
  34. data/lib/tailor/rulers/spaces_after_lbrace_ruler.rb +3 -3
  35. data/lib/tailor/rulers/spaces_after_lbracket_ruler.rb +3 -3
  36. data/lib/tailor/rulers/spaces_after_lparen_ruler.rb +3 -3
  37. data/lib/tailor/rulers/spaces_before_comma_ruler.rb +3 -3
  38. data/lib/tailor/rulers/spaces_before_rbrace_ruler.rb +2 -2
  39. data/lib/tailor/rulers/spaces_in_empty_braces_ruler.rb +2 -2
  40. data/lib/tailor/tailorrc.erb +28 -0
  41. data/lib/tailor/version.rb +1 -1
  42. data/spec/functional/conditional_parentheses_spec.rb +325 -0
  43. data/spec/functional/conditional_spacing_spec.rb +66 -42
  44. data/spec/functional/configuration_spec.rb +1 -1
  45. data/spec/functional/horizontal_spacing/braces_spec.rb +10 -9
  46. data/spec/functional/horizontal_spacing/hard_tabs_spec.rb +4 -4
  47. data/spec/functional/horizontal_spacing_spec.rb +5 -5
  48. data/spec/functional/indentation_spacing/argument_alignment_spec.rb +511 -0
  49. data/spec/functional/indentation_spacing/line_continuations_spec.rb +181 -0
  50. data/spec/functional/indentation_spacing_spec.rb +17 -0
  51. data/spec/functional/string_interpolation_spec.rb +117 -0
  52. data/spec/functional/string_quoting_spec.rb +97 -0
  53. data/spec/functional/vertical_spacing/class_length_spec.rb +1 -1
  54. data/spec/spec_helper.rb +8 -2
  55. data/spec/support/argument_alignment_cases.rb +93 -0
  56. data/spec/support/bad_indentation_cases.rb +20 -20
  57. data/spec/support/conditional_parentheses_cases.rb +60 -0
  58. data/spec/support/good_indentation_cases.rb +31 -31
  59. data/spec/support/horizontal_spacing_cases.rb +1 -1
  60. data/spec/support/line_indentation_cases.rb +71 -0
  61. data/spec/support/string_interpolation_cases.rb +45 -0
  62. data/spec/support/string_quoting_cases.rb +25 -0
  63. data/spec/unit/tailor/configuration/file_set_spec.rb +3 -3
  64. data/spec/unit/tailor/configuration/style_spec.rb +24 -21
  65. data/spec/unit/tailor/configuration_spec.rb +4 -1
  66. data/spec/unit/tailor/formatter_spec.rb +10 -10
  67. data/spec/unit/tailor/reporter_spec.rb +0 -1
  68. data/spec/unit/tailor/rulers/indentation_spaces_ruler/indentation_manager_spec.rb +0 -11
  69. data/spec/unit/tailor/rulers/indentation_spaces_ruler_spec.rb +1 -1
  70. data/spec/unit/tailor/version_spec.rb +1 -1
  71. data/tailor.gemspec +1 -0
  72. metadata +72 -33
  73. data/.infinity_test +0 -4
@@ -40,7 +40,6 @@ class Tailor
40
40
  include Tailor::Logger::Mixin
41
41
 
42
42
  attr_reader :lexer_observers
43
- attr_reader :level
44
43
 
45
44
  # @param [Object] config
46
45
  # @param [Hash] options
@@ -9,7 +9,6 @@ class Tailor
9
9
  end
10
10
 
11
11
  def ident_update(token, lexed_line, lineno, column)
12
- ident_index = lexed_line.event_index(column)
13
12
  find_event = lexed_line.find { |e| e[1] == :on_kw && e.last == 'def' }
14
13
 
15
14
  if find_event && find_event.any?
@@ -0,0 +1,65 @@
1
+ require_relative '../ruler'
2
+
3
+ class Tailor
4
+ module Rulers
5
+ class AllowConditionalParenthesesRuler < Tailor::Ruler
6
+ def initialize(style, options)
7
+ super(style, options)
8
+ add_lexer_observers :nl
9
+ end
10
+
11
+ def nl_update(current_lexed_line, lineno, _)
12
+ measure(current_lexed_line, lineno)
13
+ end
14
+
15
+ # Checks to see if a conditional is unnecessarily wrapped in parentheses.
16
+ #
17
+ # @param [Fixnum] line The current lexed line.
18
+ # @param [Fixnum] lineno Line the problem was found on.
19
+ def measure(line, lineno)
20
+ return if @config
21
+ return unless line.any? { |t| conditional?(t) }
22
+ if tokens_before_lparen?(line) and ! tokens_after_rparen?(line)
23
+ column = lparen_column(line)
24
+ @problems << Problem.new('conditional_parentheses', lineno, column,
25
+ "Parentheses around conditional expression at column #{column}.",
26
+ @options[:level])
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def conditional?(token)
33
+ token[1] == :on_kw and %w{case if unless while}.include?(token[2])
34
+ end
35
+
36
+ def lparen?(token)
37
+ token[1] == :on_lparen
38
+ end
39
+
40
+ def lparen_column(tokens)
41
+ tokens.find { |t| lparen?(t) }[0][1] + 1
42
+ end
43
+
44
+ def tokens_before_lparen?(tokens)
45
+ without_spaces(
46
+ tokens.select do |t|
47
+ true if (conditional?(t))..(lparen?(t))
48
+ end.tap { |t| t.shift; t.pop }
49
+ ).empty?
50
+ end
51
+
52
+ def tokens_after_rparen?(tokens)
53
+ without_spaces(
54
+ tokens.reverse.tap do |nl|
55
+ nl.shift
56
+ end.take_while { |t| t[1] != :on_rparen }
57
+ ).any?
58
+ end
59
+
60
+ def without_spaces(tokens)
61
+ tokens.reject { |t| t[1] == :on_sp }
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,61 @@
1
+ require_relative '../ruler'
2
+
3
+ class Tailor
4
+ module Rulers
5
+ class AllowUnnecessaryDoubleQuotesRuler < Tailor::Ruler
6
+ def initialize(config, options)
7
+ super(config, options)
8
+ add_lexer_observers :nl
9
+ end
10
+
11
+ def nl_update(lexed_line, lineno, _)
12
+ quotes(lexed_line).each do |quote|
13
+ unless contains_embedded_expression?(quote) ||
14
+ contains_escape_sequence?(quote)
15
+ measure(lineno, column(quote.first))
16
+ end
17
+ end
18
+ end
19
+
20
+ # Checks to see if the double_quotes are unnecessary.
21
+ #
22
+ # @param [Fixnum] lineno Line the problem was found on.
23
+ # @param [Fixnum] column Column the problem was found on.
24
+ def measure(lineno, column)
25
+ @problems << Problem.new('unnecessary_double_quotes', lineno, column,
26
+ "Unnecessary double quotes at column #{column}, " +
27
+ 'expected single quotes.', @options[:level])
28
+ end
29
+
30
+ private
31
+
32
+ def contains_embedded_expression?(tokens)
33
+ tokens.any? { |t| t[1] == :on_embexpr_beg }
34
+ end
35
+
36
+ def contains_escape_sequence?(tokens)
37
+ tokens.any? do |t|
38
+ t[1] == :on_tstring_content and t[2].match(/\\[a-z]+/)
39
+ end
40
+ end
41
+
42
+ def quotes(tokens)
43
+ tokens.select do |t|
44
+ true if (double_quote_start?(t))..(double_quote_end?(t))
45
+ end.slice_before { |t| double_quote_start?(t) }.reject { |q| q.empty? }
46
+ end
47
+
48
+ def column(token)
49
+ token[0][1]
50
+ end
51
+
52
+ def double_quote_start?(token)
53
+ token[1] == :on_tstring_beg and token[2] == '"'
54
+ end
55
+
56
+ def double_quote_end?(token)
57
+ token[1] == :on_tstring_end and token[2] == '"'
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,87 @@
1
+ require_relative '../ruler'
2
+
3
+ class Tailor
4
+ module Rulers
5
+ class AllowUnnecessaryInterpolationRuler < Tailor::Ruler
6
+
7
+ EVENTS = [
8
+ :on_embexpr_beg,
9
+ :on_embexpr_end,
10
+ :on_rbrace,
11
+ :on_tstring_beg,
12
+ :on_tstring_content,
13
+ :on_tstring_end
14
+ ]
15
+
16
+ def initialize(config, options)
17
+ super(config, options)
18
+ reset_tokens
19
+ add_lexer_observers :ignored_nl, :nl
20
+ end
21
+
22
+ def ignored_nl_update(lexed_line, _, _)
23
+ add_string_tokens(lexed_line)
24
+ end
25
+
26
+ def nl_update(lexed_line, _, _)
27
+ add_string_tokens(lexed_line)
28
+ each_string(@tokens).each do |string|
29
+ measure(line_number(@tokens.first), string)
30
+ end
31
+
32
+ reset_tokens
33
+ end
34
+
35
+ # Checks if variables are interpolated unnecessarily.
36
+ #
37
+ # @param [Array] tokens The filtered tokens.
38
+ def measure(lineno, tokens)
39
+ return if @config
40
+ if no_content?(tokens) and one_expression?(tokens)
41
+ @problems << Problem.new('unnecessary_string_interpolation', lineno,
42
+ column(tokens.first), 'Variable interpolated unnecessarily',
43
+ @options[:level])
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def add_string_tokens(lexed_line)
50
+ @tokens += string_tokens(lexed_line)
51
+ end
52
+
53
+ def column(token)
54
+ token.first.last + 1
55
+ end
56
+
57
+ def each_string(tokens)
58
+ tokens.select do |t|
59
+ true if (t[1] == :on_tstring_beg)..(t[1] == :on_tstring_end)
60
+ end.slice_before { |t| t[1] == :on_tstring_beg }
61
+ end
62
+
63
+ def line_number(token)
64
+ token.first.first
65
+ end
66
+
67
+ def no_content?(tokens)
68
+ ! tokens.map { |t| t[1] }.include?(:on_tstring_content)
69
+ end
70
+
71
+ def one_expression?(tokens)
72
+ tokens.select { |t| t[1] == :on_embexpr_beg }.size == 1 and
73
+ tokens.select do |t|
74
+ t[1] == :on_embexpr_end or t[1] == :on_rbrace
75
+ end.any?
76
+ end
77
+
78
+ def reset_tokens
79
+ @tokens = []
80
+ end
81
+
82
+ def string_tokens(lexed_line)
83
+ lexed_line.select { |t| EVENTS.include?(t[1]) }
84
+ end
85
+ end
86
+ end
87
+ end
@@ -1,7 +1,10 @@
1
+ require 'nokogiri'
1
2
  require_relative '../ruler'
2
3
  require_relative '../lexed_line'
3
4
  require_relative '../lexer/token'
5
+ require_relative 'indentation_spaces_ruler/argument_alignment'
4
6
  require_relative 'indentation_spaces_ruler/indentation_manager'
7
+ require_relative 'indentation_spaces_ruler/line_continuations'
5
8
 
6
9
  class Tailor
7
10
  module Rulers
@@ -12,6 +15,7 @@ class Tailor
12
15
  :comment,
13
16
  :embexpr_beg,
14
17
  :embexpr_end,
18
+ :file_beg,
15
19
  :ignored_nl,
16
20
  :kw,
17
21
  :lbrace,
@@ -67,21 +71,17 @@ class Tailor
67
71
  # More info: https://bugs.ruby-lang.org/issues/6211
68
72
  def embexpr_end_update(current_lexed_line, lineno, column)
69
73
  @embexpr_nesting.pop
70
-
71
- if @manager.multi_line_braces?(lineno)
72
- log 'End of multi-line braces!'
73
-
74
- if current_lexed_line.only_embexpr_end?
75
- @manager.amount_to_change_this -= 1
76
- msg = 'lonely embexpr_end. '
77
- msg << "change_this -= 1 -> #{@manager.amount_to_change_this}"
78
- log msg
79
- end
80
- end
81
-
82
74
  @manager.update_for_closing_reason(:on_embexpr_end, current_lexed_line)
83
75
  end
84
76
 
77
+ def file_beg_update(file_name)
78
+ # For statements that continue over multiple lines we may want to treat
79
+ # the second and subsequent lines differently and ident them further,
80
+ # controlled by the :line_continuations option.
81
+ @lines = LineContinuations.new(file_name) if line_continuations?
82
+ @args = ArgumentAlignment.new(file_name) if argument_alignment?
83
+ end
84
+
85
85
  def ignored_nl_update(current_lexed_line, lineno, column)
86
86
  log "indent reasons on entry: #{@manager.indent_reasons}"
87
87
 
@@ -110,7 +110,6 @@ class Tailor
110
110
  end
111
111
 
112
112
  @manager.update_actual_indentation(current_lexed_line)
113
- @manager.set_up_line_transition
114
113
  measure(lineno, column)
115
114
 
116
115
  log "indent reasons on exit: #{@manager.indent_reasons}"
@@ -172,8 +171,6 @@ class Tailor
172
171
  last[:event_type], current_lexed_line)
173
172
  end
174
173
 
175
- @manager.set_up_line_transition
176
-
177
174
  unless current_lexed_line.end_of_multi_line_string?
178
175
  measure(lineno, column)
179
176
  end
@@ -205,47 +202,14 @@ class Tailor
205
202
  return
206
203
  end
207
204
 
208
- if @manager.multi_line_braces?(lineno)
209
- log 'End of multi-line braces!'
210
-
211
- if current_lexed_line.only_rbrace?
212
- @manager.amount_to_change_this -= 1
213
- msg = 'lonely rbrace. '
214
- msg << "change_this -= 1 -> #{@manager.amount_to_change_this}"
215
- log msg
216
- end
217
- end
218
-
219
205
  @manager.update_for_closing_reason(:on_rbrace, current_lexed_line)
220
206
  end
221
207
 
222
208
  def rbracket_update(current_lexed_line, lineno, column)
223
- if @manager.multi_line_brackets?(lineno)
224
- log 'End of multi-line brackets!'
225
-
226
- if current_lexed_line.only_rbracket?
227
- @manager.amount_to_change_this -= 1
228
- msg = 'lonely rbracket. '
229
- msg << "change_this -= 1 -> #{@manager.amount_to_change_this}"
230
- log msg
231
- end
232
- end
233
-
234
209
  @manager.update_for_closing_reason(:on_rbracket, current_lexed_line)
235
210
  end
236
211
 
237
212
  def rparen_update(current_lexed_line, lineno, column)
238
- if @manager.multi_line_parens?(lineno)
239
- log 'End of multi-line parens!'
240
-
241
- if current_lexed_line.only_rparen?
242
- @manager.amount_to_change_this -= 1
243
- msg = 'lonely rparen. '
244
- msg << "change_this -= 1 -> #{@manager.amount_to_change_this}"
245
- log msg
246
- end
247
- end
248
-
249
213
  @manager.update_for_closing_reason(:on_rparen, current_lexed_line)
250
214
  end
251
215
 
@@ -272,6 +236,29 @@ class Tailor
272
236
  !@tstring_nesting.empty?
273
237
  end
274
238
 
239
+ def line_continuations?
240
+ @options[:line_continuations] and @options[:line_continuations] != :off
241
+ end
242
+
243
+ def with_line_continuations(lineno, should_be_at)
244
+ return should_be_at unless line_continuations?
245
+ if @lines.line_is_continuation?(lineno) and
246
+ @lines.line_has_nested_statements?(lineno)
247
+ should_be_at + @config
248
+ else
249
+ should_be_at
250
+ end
251
+ end
252
+
253
+ def argument_alignment?
254
+ @options[:argument_alignment] and @options[:argument_alignment] != :off
255
+ end
256
+
257
+ def with_argument_alignment(lineno, should_be_at)
258
+ return should_be_at unless argument_alignment?
259
+ @args.expected_column(lineno, should_be_at)
260
+ end
261
+
275
262
  # Checks if the line's indentation level is appropriate.
276
263
  #
277
264
  # @param [Fixnum] lineno The line the potential problem is on.
@@ -279,9 +266,12 @@ class Tailor
279
266
  def measure(lineno, column)
280
267
  log 'Measuring...'
281
268
 
282
- if @manager.actual_indentation != @manager.should_be_at
269
+ should_be_at = with_line_continuations(lineno, @manager.should_be_at)
270
+ should_be_at = with_argument_alignment(lineno, should_be_at)
271
+
272
+ if @manager.actual_indentation != should_be_at
283
273
  msg = "Line is indented to column #{@manager.actual_indentation}, "
284
- msg << "but should be at #{@manager.should_be_at}."
274
+ msg << "but should be at #{should_be_at}."
285
275
 
286
276
  @problems << Problem.new(problem_type, lineno,
287
277
  @manager.actual_indentation, msg, @options[:level])
@@ -0,0 +1,63 @@
1
+ require 'ripper'
2
+ require_relative './ast_xml'
3
+
4
+ class Tailor
5
+ module Rulers
6
+ class IndentationSpacesRuler < Tailor::Ruler
7
+
8
+ # Determines whether arguments spread across lines are correctly
9
+ # aligned.
10
+ #
11
+ # Function calls with parentheses could be determined easily from
12
+ # the lexed line only but we need to support calls without parentheses
13
+ # too.
14
+ class ArgumentAlignment
15
+
16
+ include AstXml
17
+
18
+ def initialize(file_name)
19
+ @ast = build_xml(Ripper::SexpBuilder.new(File.read(file_name)).parse)
20
+ @lex = Ripper.lex(File.read(file_name))
21
+ end
22
+
23
+ def expected_column(lineno, should_be_at)
24
+ column = call_column(lineno) || declaration_column(lineno)
25
+ correct_for_literals(lineno, column) || should_be_at
26
+ end
27
+
28
+ private
29
+
30
+ # sexp column offsets for string literals do not include the quote
31
+ def correct_for_literals(lineno, column)
32
+ tstring_index = @lex.index do |pos, token|
33
+ pos[0] == lineno and pos[1] == column and
34
+ token == :on_tstring_content
35
+ end
36
+
37
+ tstring_index ? @lex[tstring_index -1][0][1] : column
38
+ end
39
+
40
+ def call_column(lineno)
41
+ [
42
+ first_argument(:command_call, :args_add_block, lineno),
43
+ first_argument(:method_add_arg, :args_add_block, lineno)
44
+ ].compact.min
45
+ end
46
+
47
+ def declaration_column(lineno)
48
+ first_argument(:def, :params, lineno)
49
+ end
50
+
51
+ def first_argument(parent, child, lineno)
52
+ method_defs = @ast.xpath("//#{parent}")
53
+ method_defs.map do |d|
54
+ d.xpath("descendant::#{child}[descendant::pos[@line = #{lineno}
55
+ ]]/descendant::pos[position() = 1 and @line != #{lineno}]/
56
+ @column").first.to_s.to_i
57
+ end.reject { |c| c == 0 }.min
58
+ end
59
+
60
+ end
61
+ end
62
+ end
63
+ end