tailor 1.3.1 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
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