scss-lint 0.25.1 → 0.26.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +10 -1
  3. data/data/property-sort-orders/concentric.txt +99 -0
  4. data/lib/scss_lint.rb +1 -0
  5. data/lib/scss_lint/cli.rb +9 -3
  6. data/lib/scss_lint/exceptions.rb +4 -0
  7. data/lib/scss_lint/linter.rb +10 -1
  8. data/lib/scss_lint/linter/capitalization_in_selector.rb +14 -6
  9. data/lib/scss_lint/linter/compass/property_with_mixin.rb +9 -2
  10. data/lib/scss_lint/linter/indentation.rb +28 -6
  11. data/lib/scss_lint/linter/property_sort_order.rb +61 -9
  12. data/lib/scss_lint/linter/single_line_per_property.rb +53 -0
  13. data/lib/scss_lint/linter/single_line_per_selector.rb +6 -1
  14. data/lib/scss_lint/linter/space_after_comma.rb +27 -19
  15. data/lib/scss_lint/linter/space_before_brace.rb +5 -4
  16. data/lib/scss_lint/linter/trailing_semicolon.rb +53 -0
  17. data/lib/scss_lint/linter/unnecessary_parent_reference.rb +36 -0
  18. data/lib/scss_lint/reporter/default_reporter.rb +7 -2
  19. data/lib/scss_lint/reporter/xml_reporter.rb +2 -1
  20. data/lib/scss_lint/runner.rb +7 -3
  21. data/lib/scss_lint/version.rb +1 -1
  22. data/spec/scss_lint/cli_spec.rb +314 -0
  23. data/spec/scss_lint/config_spec.rb +439 -0
  24. data/spec/scss_lint/engine_spec.rb +24 -0
  25. data/spec/scss_lint/linter/border_zero_spec.rb +84 -0
  26. data/spec/scss_lint/linter/capitalization_in_selector_spec.rb +71 -0
  27. data/spec/scss_lint/linter/color_keyword_spec.rb +83 -0
  28. data/spec/scss_lint/linter/comment_spec.rb +55 -0
  29. data/spec/scss_lint/linter/compass/property_with_mixin_spec.rb +55 -0
  30. data/spec/scss_lint/linter/debug_statement_spec.rb +21 -0
  31. data/spec/scss_lint/linter/declaration_order_spec.rb +94 -0
  32. data/spec/scss_lint/linter/duplicate_property_spec.rb +176 -0
  33. data/spec/scss_lint/linter/else_placement_spec.rb +106 -0
  34. data/spec/scss_lint/linter/empty_line_between_blocks_spec.rb +263 -0
  35. data/spec/scss_lint/linter/empty_rule_spec.rb +27 -0
  36. data/spec/scss_lint/linter/final_newline_spec.rb +49 -0
  37. data/spec/scss_lint/linter/hex_length_spec.rb +104 -0
  38. data/spec/scss_lint/linter/hex_notation_spec.rb +104 -0
  39. data/spec/scss_lint/linter/hex_validation_spec.rb +36 -0
  40. data/spec/scss_lint/linter/id_with_extraneous_selector_spec.rb +139 -0
  41. data/spec/scss_lint/linter/indentation_spec.rb +242 -0
  42. data/spec/scss_lint/linter/leading_zero_spec.rb +233 -0
  43. data/spec/scss_lint/linter/mergeable_selector_spec.rb +283 -0
  44. data/spec/scss_lint/linter/name_format_spec.rb +206 -0
  45. data/spec/scss_lint/linter/placeholder_in_extend_spec.rb +63 -0
  46. data/spec/scss_lint/linter/property_sort_order_spec.rb +246 -0
  47. data/spec/scss_lint/linter/property_spelling_spec.rb +57 -0
  48. data/spec/scss_lint/linter/selector_depth_spec.rb +159 -0
  49. data/spec/scss_lint/linter/shorthand_spec.rb +172 -0
  50. data/spec/scss_lint/linter/single_line_per_property_spec.rb +73 -0
  51. data/spec/scss_lint/linter/single_line_per_selector_spec.rb +121 -0
  52. data/spec/scss_lint/linter/space_after_comma_spec.rb +315 -0
  53. data/spec/scss_lint/linter/space_after_property_colon_spec.rb +238 -0
  54. data/spec/scss_lint/linter/space_after_property_name_spec.rb +23 -0
  55. data/spec/scss_lint/linter/space_before_brace_spec.rb +447 -0
  56. data/spec/scss_lint/linter/space_between_parens_spec.rb +263 -0
  57. data/spec/scss_lint/linter/string_quotes_spec.rb +303 -0
  58. data/spec/scss_lint/linter/trailing_semicolon_spec.rb +188 -0
  59. data/spec/scss_lint/linter/unnecessary_mantissa_spec.rb +67 -0
  60. data/spec/scss_lint/linter/unnecessary_parent_reference_spec.rb +67 -0
  61. data/spec/scss_lint/linter/url_format_spec.rb +55 -0
  62. data/spec/scss_lint/linter/url_quotes_spec.rb +63 -0
  63. data/spec/scss_lint/linter/zero_unit_spec.rb +113 -0
  64. data/spec/scss_lint/linter_registry_spec.rb +50 -0
  65. data/spec/scss_lint/location_spec.rb +42 -0
  66. data/spec/scss_lint/reporter/config_reporter_spec.rb +42 -0
  67. data/spec/scss_lint/reporter/default_reporter_spec.rb +73 -0
  68. data/spec/scss_lint/reporter/files_reporter_spec.rb +38 -0
  69. data/spec/scss_lint/reporter/xml_reporter_spec.rb +103 -0
  70. data/spec/scss_lint/reporter_spec.rb +11 -0
  71. data/spec/scss_lint/runner_spec.rb +132 -0
  72. data/spec/scss_lint/selector_visitor_spec.rb +264 -0
  73. data/spec/spec_helper.rb +34 -0
  74. data/spec/support/isolated_environment.rb +25 -0
  75. data/spec/support/matchers/report_lint.rb +48 -0
  76. metadata +126 -8
  77. data/lib/scss_lint/linter/trailing_semicolon_after_property_value.rb +0 -40
@@ -0,0 +1,53 @@
1
+ module SCSSLint
2
+ # Checks that all properties in a rule set are on their own distinct lines.
3
+ class Linter::SingleLinePerProperty < Linter
4
+ include LinterRegistry
5
+
6
+ def visit_rule(node) # rubocop:disable CyclomaticComplexity
7
+ single_line = single_line_rule_set?(node)
8
+ return if single_line && config['allow_single_line_rule_sets']
9
+
10
+ properties = node.children.select { |child| child.is_a?(Sass::Tree::PropNode) }
11
+ return unless properties.any?
12
+
13
+ # Special case: if single line rule sets aren't allowed, we want to report
14
+ # when the first property isn't on a separate line from the selector
15
+ if single_line && !config['allow_single_line_rule_sets']
16
+ add_lint(properties.first,
17
+ "Property '#{properties.first.name.join}' should be placed " \
18
+ 'on separate line from selector')
19
+ end
20
+
21
+ # Compare each property against the next property to see if they are on
22
+ # the same line
23
+ properties[0..-2].zip(properties[1..-1]).each do |first, second|
24
+ next unless first.line == second.line
25
+
26
+ add_lint(second, "Property '#{second.name.join}' should be placed on own line")
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ # Return whether this rule set occupies a single line.
33
+ #
34
+ # Note that this allows:
35
+ # a,
36
+ # b,
37
+ # i { margin: 0; padding: 0; }
38
+ #
39
+ # and:
40
+ #
41
+ # p { margin: 0; padding: 0; }
42
+ #
43
+ # In other words, the line of the opening curly brace is the line that the
44
+ # rule set is considered to occupy.
45
+ def single_line_rule_set?(rule)
46
+ rule.children.all? { |child| child.line == rule.source_range.end_pos.line }
47
+ end
48
+
49
+ def first_property_not_on_own_line?(rule, properties)
50
+ properties.any? && properties.first.line == rule.line
51
+ end
52
+ end
53
+ end
@@ -14,6 +14,11 @@ module SCSSLint
14
14
 
15
15
  # A comma is invalid if it starts the line or is not the end of the line
16
16
  def invalid_comma_placement?(node)
17
+ # We must ignore selectors with interpolation, since there's no way to
18
+ # tell if the overall selector is valid since the interpolation could
19
+ # insert commas incorrectly. Thus we simply ignore.
20
+ return if node.rule.any? { |item| item.is_a?(Sass::Script::Variable) }
21
+
17
22
  normalize_spacing(condense_to_string(node.rule)) =~ /\n,|,[^\n]/
18
23
  end
19
24
 
@@ -21,7 +26,7 @@ module SCSSLint
21
26
  # Sass::Script::Nodes, we need to condense it into a single string that we
22
27
  # can run a regex against.
23
28
  def condense_to_string(sequence_list)
24
- sequence_list.select { |item| item.is_a?(String) }.inject(:+) || ''
29
+ sequence_list.select { |item| item.is_a?(String) }.inject(:+)
25
30
  end
26
31
 
27
32
  # Removes extra spacing between lines in a comma-separated sequence due to
@@ -66,34 +66,42 @@ module SCSSLint
66
66
  def check_commas_after_args(args, arg_type)
67
67
  # For each arg except the last, check the character following the comma
68
68
  args[0..-2].each do |arg|
69
- offset = 0
70
-
71
- # Find the comma following this argument.
72
- # The Sass parser is unpredictable in where it marks the end of the
73
- # source range. Thus we need to start at the indicated range, and check
74
- # left and right of that range, gradually moving further outward until
75
- # we find the comma.
76
- if character_at(arg.source_range.end_pos, offset) != ','
77
- loop do
78
- offset += 1
79
- break if character_at(arg.source_range.end_pos, offset) == ','
80
- offset = -offset
81
- break if character_at(arg.source_range.end_pos, offset) == ','
82
- offset = -offset
83
- end
84
- end
69
+ offset = find_comma_offset(arg)
85
70
 
86
- # Check for space or newline after arg (we allow arguments to be split
71
+ # Check for space or newline after comma (we allow arguments to be split
87
72
  # up over multiple lines).
88
73
  spaces = 0
89
- while character_at(arg.source_range.end_pos, offset + 1) =~ / |\n/
74
+ while (char = character_at(arg.source_range.end_pos, offset + 1)) == ' '
90
75
  spaces += 1
91
76
  offset += 1
92
77
  end
93
- next if spaces == EXPECTED_SPACES_AFTER_COMMA
78
+ next if char == "\n" || # Ignore trailing spaces
79
+ spaces == EXPECTED_SPACES_AFTER_COMMA
94
80
 
95
81
  add_lint arg, "Commas in #{arg_type} should be followed by a single space"
96
82
  end
97
83
  end
84
+
85
+ # Find the comma following this argument.
86
+ #
87
+ # The Sass parser is unpredictable in where it marks the end of the
88
+ # source range. Thus we need to start at the indicated range, and check
89
+ # left and right of that range, gradually moving further outward until
90
+ # we find the comma.
91
+ def find_comma_offset(arg)
92
+ offset = 0
93
+
94
+ if character_at(arg.source_range.end_pos, offset) != ','
95
+ loop do
96
+ offset += 1
97
+ break if character_at(arg.source_range.end_pos, offset) == ','
98
+ offset = -offset
99
+ break if character_at(arg.source_range.end_pos, offset) == ','
100
+ offset = -offset
101
+ end
102
+ end
103
+
104
+ offset
105
+ end
98
106
  end
99
107
  end
@@ -27,16 +27,17 @@ module SCSSLint
27
27
 
28
28
  def check_for_space(node, string)
29
29
  line = node.source_range.end_pos.line
30
+ char_before_is_whitespace = ["\n", ' '].include?(string[-2])
30
31
 
31
32
  if config['allow_single_line_padding'] && node_on_single_line?(node)
32
- if string[-2] != ' '
33
+ unless char_before_is_whitespace
33
34
  add_lint(line, 'Opening curly brace `{` should be ' \
34
- 'preceded by at least one space')
35
+ 'preceded by at least one space')
35
36
  end
36
37
  else
37
- if string[-2] != ' ' || string[-3] == ' '
38
+ if !char_before_is_whitespace || string[-3] == ' '
38
39
  add_lint(line, 'Opening curly brace `{` should be ' \
39
- 'preceded by one space')
40
+ 'preceded by one space')
40
41
  end
41
42
  end
42
43
  end
@@ -0,0 +1,53 @@
1
+ module SCSSLint
2
+ # Checks for a trailing semicolon on statements within rule sets.
3
+ class Linter::TrailingSemicolon < Linter
4
+ include LinterRegistry
5
+
6
+ def visit_prop(node)
7
+ if has_nested_properties?(node)
8
+ yield # Continue checking children
9
+ else
10
+ check_semicolon(node)
11
+ end
12
+ end
13
+
14
+ def visit_extend(node)
15
+ check_semicolon(node)
16
+ end
17
+
18
+ def visit_mixin(node)
19
+ check_semicolon(node)
20
+ end
21
+
22
+ def visit_variable(node)
23
+ check_semicolon(node)
24
+ end
25
+
26
+ private
27
+
28
+ def has_nested_properties?(node)
29
+ node.children.any? { |n| n.is_a?(Sass::Tree::PropNode) }
30
+ end
31
+
32
+ def check_semicolon(node)
33
+ if has_space_before_semicolon?(node)
34
+ line = node.source_range.start_pos.line
35
+ add_lint line, 'Declaration should be terminated by a semicolon'
36
+ elsif !ends_with_semicolon?(node)
37
+ line = node.source_range.start_pos.line
38
+ add_lint line,
39
+ 'Declaration should not have a space before ' \
40
+ 'the terminating semicolon'
41
+ end
42
+ end
43
+
44
+ # Checks that the node is ended by a semicolon (with no whitespace)
45
+ def ends_with_semicolon?(node)
46
+ source_from_range(node.source_range) =~ /;$/
47
+ end
48
+
49
+ def has_space_before_semicolon?(node)
50
+ source_from_range(node.source_range) =~ /\s;$/
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,36 @@
1
+ module SCSSLint
2
+ # Checks for unnecessary uses of the parent reference (&) in nested selectors.
3
+ class Linter::UnnecessaryParentReference < Linter
4
+ include LinterRegistry
5
+
6
+ MESSAGE = 'Unnecessary parent selector'
7
+
8
+ def visit_comma_sequence(comma_sequence)
9
+ @multiple_sequences = comma_sequence.members.size > 1
10
+ end
11
+
12
+ def visit_sequence(sequence)
13
+ return unless sequence_starts_with_parent?(sequence.members.first)
14
+
15
+ # Special case: allow an isolated parent to appear if it is part of a
16
+ # comma sequence of more than one sequence, as this could be used to DRY
17
+ # up code.
18
+ return if @multiple_sequences && isolated_parent?(sequence)
19
+
20
+ add_lint(sequence.members.first.line, MESSAGE)
21
+ end
22
+
23
+ private
24
+
25
+ def isolated_parent?(sequence)
26
+ sequence.members.size == 1 &&
27
+ sequence_starts_with_parent?(sequence.members.first)
28
+ end
29
+
30
+ def sequence_starts_with_parent?(simple_sequence)
31
+ return unless simple_sequence.is_a?(Sass::Selector::SimpleSequence)
32
+ simple_sequence.members.size == 1 &&
33
+ simple_sequence.members.first.is_a?(Sass::Selector::Parent)
34
+ end
35
+ end
36
+ end
@@ -6,8 +6,13 @@ module SCSSLint
6
6
 
7
7
  lints.map do |lint|
8
8
  type = lint.error? ? '[E]'.color(:red) : '[W]'.color(:yellow)
9
- "#{lint.filename.color(:cyan)}:" << "#{lint.location.line}".color(:magenta) <<
10
- " #{type} #{lint.description}"
9
+
10
+ linter_name = "#{lint.linter.name}: ".color(:green) if lint.linter
11
+ message = "#{linter_name}#{lint.description}"
12
+
13
+ "#{lint.filename.color(:cyan)}:" <<
14
+ "#{lint.location.line}".color(:magenta) <<
15
+ " #{type} #{message}"
11
16
  end.join("\n") + "\n"
12
17
  end
13
18
  end
@@ -9,7 +9,8 @@ module SCSSLint
9
9
  output << "<file name=#{filename.encode(xml: :attr)}>"
10
10
 
11
11
  file_lints.each do |lint|
12
- output << "<issue line=\"#{lint.location.line}\" " \
12
+ output << "<issue linter=\"#{lint.linter.name if lint.linter}\" " \
13
+ "line=\"#{lint.location.line}\" " \
13
14
  "column=\"#{lint.location.column}\" " \
14
15
  "length=\"#{lint.location.length}\" " \
15
16
  "severity=\"#{lint.severity}\" " \
@@ -1,5 +1,4 @@
1
1
  module SCSSLint
2
- class LinterError < StandardError; end
3
2
  class NoFilesError < StandardError; end
4
3
 
5
4
  # Finds and aggregates all lints found by running the registered linters
@@ -40,9 +39,9 @@ module SCSSLint
40
39
  next if config.excluded_file_for_linter?(file, linter)
41
40
 
42
41
  begin
43
- linter.run(engine, config.linter_options(linter))
42
+ run_linter(linter, engine, config)
44
43
  rescue => error
45
- raise LinterError,
44
+ raise SCSSLint::Exceptions::LinterError,
46
45
  "#{linter.class} raised unexpected error linting file #{file}: " \
47
46
  "'#{error.message}'",
48
47
  error.backtrace
@@ -53,5 +52,10 @@ module SCSSLint
53
52
  rescue FileEncodingError => ex
54
53
  @lints << Lint.new(nil, file, Location.new, ex.to_s, :error)
55
54
  end
55
+
56
+ # For stubbing in tests.
57
+ def run_linter(linter, engine, config)
58
+ linter.run(engine, config.linter_options(linter))
59
+ end
56
60
  end
57
61
  end
@@ -1,4 +1,4 @@
1
1
  # Defines the gem version.
2
2
  module SCSSLint
3
- VERSION = '0.25.1'
3
+ VERSION = '0.26.0'
4
4
  end
@@ -0,0 +1,314 @@
1
+ require 'spec_helper'
2
+ require 'scss_lint/cli'
3
+
4
+ describe SCSSLint::CLI do
5
+ let(:config_options) do
6
+ {
7
+ 'linters' => {
8
+ 'FakeTestLinter1' => { 'enabled' => true },
9
+ 'FakeTestLinter2' => { 'enabled' => true },
10
+ },
11
+ }
12
+ end
13
+
14
+ let(:config) { SCSSLint::Config.new(config_options) }
15
+
16
+ class SCSSLint::Linter::FakeTestLinter1 < SCSSLint::Linter; end
17
+ class SCSSLint::Linter::FakeTestLinter2 < SCSSLint::Linter; end
18
+
19
+ before do
20
+ # Silence console output
21
+ @output = ''
22
+ STDOUT.stub(:write) { |*args| @output.<<(*args) }
23
+
24
+ SCSSLint::Config.stub(:load).and_return(config)
25
+ SCSSLint::LinterRegistry.stub(:linters)
26
+ .and_return([SCSSLint::Linter::FakeTestLinter1,
27
+ SCSSLint::Linter::FakeTestLinter2])
28
+ end
29
+
30
+ describe '#parse_arguments' do
31
+ let(:files) { ['file1.scss', 'file2.scss'] }
32
+ let(:flags) { [] }
33
+ subject { SCSSLint::CLI.new(flags + files) }
34
+
35
+ def safe_parse
36
+ subject.parse_arguments
37
+ rescue SystemExit
38
+ # Keep running tests
39
+ end
40
+
41
+ context 'when the config_file flag is set' do
42
+ let(:config_file) { 'my-config-file.yml' }
43
+ let(:flags) { ['-c', config_file] }
44
+
45
+ it 'loads that config file' do
46
+ SCSSLint::Config.should_receive(:load).with(config_file)
47
+ safe_parse
48
+ end
49
+
50
+ context 'and the config file is invalid' do
51
+ before do
52
+ SCSSLint::Config.should_receive(:load)
53
+ .with(config_file)
54
+ .and_raise(SCSSLint::InvalidConfiguration)
55
+ end
56
+
57
+ it 'halts with a configuration error code' do
58
+ subject.should_receive(:halt).with(:config)
59
+ safe_parse
60
+ end
61
+ end
62
+ end
63
+
64
+ context 'when the excluded files flag is set' do
65
+ let(:flags) { ['-e', 'file1.scss,file3.scss'] }
66
+
67
+ it 'sets the :excluded_files option' do
68
+ safe_parse
69
+ subject.options[:excluded_files].should =~ ['file1.scss', 'file3.scss']
70
+ end
71
+ end
72
+
73
+ context 'when the include linters flag is set' do
74
+ let(:flags) { %w[-i FakeTestLinter2] }
75
+
76
+ it 'enables only the included linters' do
77
+ safe_parse
78
+ subject.config.enabled_linters.should == [SCSSLint::Linter::FakeTestLinter2]
79
+ end
80
+
81
+ context 'and the included linter does not exist' do
82
+ let(:flags) { %w[-i NonExistentLinter] }
83
+
84
+ it 'halts with a configuration error code' do
85
+ subject.should_receive(:halt).with(:config)
86
+ safe_parse
87
+ end
88
+ end
89
+ end
90
+
91
+ context 'when the exclude linters flag is set' do
92
+ let(:flags) { %w[-x FakeTestLinter1] }
93
+
94
+ it 'includes all linters except the excluded one' do
95
+ safe_parse
96
+ subject.config.enabled_linters.should == [SCSSLint::Linter::FakeTestLinter2]
97
+ end
98
+ end
99
+
100
+ context 'when the format flag is set' do
101
+ context 'and the format is valid' do
102
+ let(:flags) { %w[--format XML] }
103
+
104
+ it 'sets the :reporter option to the correct reporter' do
105
+ safe_parse
106
+ subject.options[:reporter].should == SCSSLint::Reporter::XMLReporter
107
+ end
108
+ end
109
+
110
+ context 'and the format is invalid' do
111
+ let(:flags) { %w[--format InvalidFormat] }
112
+
113
+ it 'sets the :reporter option to the correct reporter' do
114
+ subject.should_receive(:halt).with(:config)
115
+ safe_parse
116
+ end
117
+ end
118
+ end
119
+
120
+ context 'when the show formatters flag is set' do
121
+ let(:flags) { ['--show-formatters'] }
122
+
123
+ it 'prints the formatters' do
124
+ subject.should_receive(:print_formatters)
125
+ safe_parse
126
+ end
127
+ end
128
+
129
+ context 'when the show linters flag is set' do
130
+ let(:flags) { ['--show-linters'] }
131
+
132
+ it 'prints the linters' do
133
+ subject.should_receive(:print_linters)
134
+ safe_parse
135
+ end
136
+ end
137
+
138
+ context 'when the help flag is set' do
139
+ let(:flags) { ['-h'] }
140
+
141
+ it 'prints a help message' do
142
+ subject.should_receive(:print_help)
143
+ safe_parse
144
+ end
145
+ end
146
+
147
+ context 'when the version flag is set' do
148
+ let(:flags) { ['-v'] }
149
+
150
+ it 'prints the program version' do
151
+ subject.should_receive(:print_version)
152
+ safe_parse
153
+ end
154
+ end
155
+
156
+ context 'when an invalid option is specified' do
157
+ let(:flags) { ['--non-existant-option'] }
158
+
159
+ it 'prints a help message' do
160
+ subject.should_receive(:print_help)
161
+ safe_parse
162
+ end
163
+ end
164
+
165
+ context 'when no files are specified' do
166
+ let(:files) { [] }
167
+
168
+ it 'sets :files option to the empty list' do
169
+ safe_parse
170
+ subject.options[:files].should be_empty
171
+ end
172
+ end
173
+
174
+ context 'when files are specified' do
175
+ it 'sets :files option to the list of files' do
176
+ safe_parse
177
+ subject.options[:files].should =~ files
178
+ end
179
+ end
180
+ end
181
+
182
+ describe '#run' do
183
+ let(:files) { ['file1.scss', 'file2.scss'] }
184
+ let(:options) { {} }
185
+ subject { SCSSLint::CLI.new(options) }
186
+
187
+ before do
188
+ subject.stub(:extract_files_from).and_return(files)
189
+ end
190
+
191
+ def safe_run
192
+ subject.run
193
+ rescue SystemExit
194
+ # Keep running tests
195
+ end
196
+
197
+ context 'when no files are specified' do
198
+ let(:files) { [] }
199
+
200
+ it 'exits with a no-input status code' do
201
+ subject.should_receive(:halt).with(:no_input)
202
+ safe_run
203
+ end
204
+ end
205
+
206
+ context 'when files are specified' do
207
+ it 'passes the set of files to the runner' do
208
+ SCSSLint::Runner.any_instance.should_receive(:run).with(files)
209
+ safe_run
210
+ end
211
+
212
+ it 'uses the default reporter' do
213
+ SCSSLint::Reporter::DefaultReporter.any_instance
214
+ .should_receive(:report_lints)
215
+ safe_run
216
+ end
217
+ end
218
+
219
+ context 'when there are no lints' do
220
+ before do
221
+ SCSSLint::Runner.any_instance.stub(:lints).and_return([])
222
+ end
223
+
224
+ it 'exits cleanly' do
225
+ subject.should_not_receive(:halt)
226
+ safe_run
227
+ end
228
+
229
+ it 'outputs nothing' do
230
+ safe_run
231
+ @output.should be_empty
232
+ end
233
+ end
234
+
235
+ context 'when there are only warnings' do
236
+ before do
237
+ SCSSLint::Runner.any_instance.stub(:lints).and_return([
238
+ SCSSLint::Lint.new(
239
+ SCSSLint::Linter::FakeTestLinter1.new,
240
+ 'some-file.scss',
241
+ SCSSLint::Location.new(1, 1, 1),
242
+ 'Some description',
243
+ :warning,
244
+ ),
245
+ ])
246
+ end
247
+
248
+ it 'exits cleanly' do
249
+ subject.should_receive(:halt).with(:warning)
250
+ safe_run
251
+ end
252
+
253
+ it 'outputs the warnings' do
254
+ safe_run
255
+ @output.should include 'Some description'
256
+ end
257
+ end
258
+
259
+ context 'when there are errors' do
260
+ before do
261
+ SCSSLint::Runner.any_instance.stub(:lints).and_return([
262
+ SCSSLint::Lint.new(
263
+ SCSSLint::Linter::FakeTestLinter1.new,
264
+ 'some-file.scss',
265
+ SCSSLint::Location.new(1, 1, 1),
266
+ 'Some description',
267
+ :error,
268
+ ),
269
+ ])
270
+ end
271
+
272
+ it 'exits with an error status code' do
273
+ subject.should_receive(:halt).with(:error)
274
+ safe_run
275
+ end
276
+
277
+ it 'outputs the errors' do
278
+ safe_run
279
+ @output.should include 'Some description'
280
+ end
281
+ end
282
+
283
+ context 'when the runner raises an error' do
284
+ let(:backtrace) { %w[file1.rb file2.rb] }
285
+ let(:message) { 'Some error message' }
286
+
287
+ let(:error) do
288
+ StandardError.new(message).tap { |e| e.set_backtrace(backtrace) }
289
+ end
290
+
291
+ before { SCSSLint::Runner.stub(:new).and_raise(error) }
292
+
293
+ it 'exits with an internal software error status code' do
294
+ subject.should_receive(:halt).with(:software)
295
+ safe_run
296
+ end
297
+
298
+ it 'outputs the error message' do
299
+ safe_run
300
+ @output.should include message
301
+ end
302
+
303
+ it 'outputs the backtrace' do
304
+ safe_run
305
+ @output.should include backtrace.join("\n")
306
+ end
307
+
308
+ it 'outputs a link to the issue tracker' do
309
+ safe_run
310
+ @output.should include SCSSLint::BUG_REPORT_URL
311
+ end
312
+ end
313
+ end
314
+ end