scss-lint 0.25.1 → 0.26.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 (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