scss-lint 0.6.7 → 0.7.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.
data/lib/sass/tree.rb ADDED
@@ -0,0 +1,148 @@
1
+ # Contains extensions of Sass::Tree::Nodes to add support for traversing the
2
+ # Sass::Script::Node parse trees contained within the nodes. This probably
3
+ # breaks the Sass compiler, but since we're only doing lints this is fine for
4
+ # now.
5
+ module Sass::Tree
6
+ # Define some common helper code for use in the various monkey patchings.
7
+ class Node
8
+ # The `args` field of some Sass::Tree::Node classes returns
9
+ # Sass::Script::Variable nodes with no line numbers. This adds the line
10
+ # numbers back in so lint reporting works for those nodes.
11
+ def add_line_numbers_to_args(arg_list)
12
+ arg_list.each do |variable, default_expr|
13
+ variable.line = line # Use line number of containing parse tree node
14
+ end
15
+ end
16
+
17
+ # Sometimes the parse tree doesn't return a Sass::Script::Variable, but just
18
+ # the name of the variable. This helper takes that name and turns it back
19
+ # into a Sass::Script::Variable that supports lint reporting.
20
+ def create_variable(var_name)
21
+ Sass::Script::Variable.new(var_name).tap do |v|
22
+ v.line = line # Use line number of the containing parse tree node
23
+ end
24
+ end
25
+
26
+ # A number of tree nodes return lists that have strings and
27
+ # Sass::Script::Nodes interspersed within them. This returns a filtered list
28
+ # of just those nodes.
29
+ def extract_script_nodes(list)
30
+ list.select { |item| item.is_a? Sass::Script::Node }
31
+ end
32
+
33
+ # Takes a list of arguments, be they arrays or individual objects, and
34
+ # returns a single flat list that can be passed to
35
+ # Sass::Tree::Visitors::Base#visit_children.
36
+ def concat_expr_lists(*expr_lists)
37
+ expr_lists.flatten.compact
38
+ end
39
+ end
40
+
41
+ class CommentNode
42
+ def children
43
+ concat_expr_lists super, extract_script_nodes(value)
44
+ end
45
+ end
46
+
47
+ class DebugNode
48
+ def children
49
+ concat_expr_lists super, expr
50
+ end
51
+ end
52
+
53
+ class DirectiveNode
54
+ def children
55
+ concat_expr_lists super, extract_script_nodes(value)
56
+ end
57
+ end
58
+
59
+ class EachNode
60
+ def children
61
+ concat_expr_lists super, create_variable(var), list
62
+ end
63
+ end
64
+
65
+ class ExtendNode
66
+ def children
67
+ concat_expr_lists super, extract_script_nodes(selector)
68
+ end
69
+ end
70
+
71
+ class ForNode
72
+ def children
73
+ concat_expr_lists super, create_variable(var), from, to
74
+ end
75
+ end
76
+
77
+ class FunctionNode
78
+ def children
79
+ add_line_numbers_to_args(args)
80
+
81
+ concat_expr_lists super, args, splat
82
+ end
83
+ end
84
+
85
+ class IfNode
86
+ def children
87
+ concat_expr_lists super, expr
88
+ end
89
+ end
90
+
91
+ class MixinDefNode
92
+ def children
93
+ add_line_numbers_to_args(args)
94
+
95
+ concat_expr_lists super, args, splat
96
+ end
97
+ end
98
+
99
+ class MixinNode
100
+ def children
101
+ add_line_numbers_to_args(args)
102
+
103
+ # Keyword mapping is String -> Expr, so convert the string to a variable
104
+ # node that supports lint reporting
105
+ keyword_exprs = keywords.map do |var_name, var_expr|
106
+ [create_variable(var_name), var_expr]
107
+ end
108
+
109
+ concat_expr_lists super, args, keyword_exprs, splat
110
+ end
111
+ end
112
+
113
+ class PropNode
114
+ def children
115
+ concat_expr_lists super, extract_script_nodes(name), value
116
+ end
117
+ end
118
+
119
+ class ReturnNode
120
+ def children
121
+ concat_expr_lists super, expr
122
+ end
123
+ end
124
+
125
+ class RuleNode
126
+ def children
127
+ concat_expr_lists super, extract_script_nodes(rule)
128
+ end
129
+ end
130
+
131
+ class VariableNode
132
+ def children
133
+ concat_expr_lists super, expr
134
+ end
135
+ end
136
+
137
+ class WarnNode
138
+ def children
139
+ concat_expr_lists super, expr
140
+ end
141
+ end
142
+
143
+ class WhileNode
144
+ def children
145
+ concat_expr_lists super, expr
146
+ end
147
+ end
148
+ end
@@ -4,10 +4,11 @@ module SCSSLint
4
4
  class Engine
5
5
  ENGINE_OPTIONS = { cache: false, syntax: :scss }
6
6
 
7
- attr_reader :contents, :lines, :tree
7
+ attr_reader :contents, :filename, :lines, :tree
8
8
 
9
9
  def initialize(scss_or_filename)
10
10
  if File.exists?(scss_or_filename)
11
+ @filename = scss_or_filename
11
12
  @engine = Sass::Engine.for_file(scss_or_filename, ENGINE_OPTIONS)
12
13
  @contents = File.open(scss_or_filename, 'r').read
13
14
  else
@@ -0,0 +1,24 @@
1
+ require 'sass'
2
+
3
+ module SCSSLint
4
+ class Linter::BorderZeroLinter < Linter
5
+ include LinterRegistry
6
+
7
+ def visit_prop(node)
8
+ return unless BORDER_PROPERTIES.include? node.name.first.to_s
9
+ add_lint(node) if node.value.to_sass.strip == 'none'
10
+ end
11
+
12
+ def description
13
+ '`border: 0;` is preferred over `border: none;`'
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ BORDER_PROPERTIES = %w[border
20
+ border-top
21
+ border-right
22
+ border-bottom
23
+ border-left]
24
+ end
@@ -0,0 +1,15 @@
1
+ require 'sass'
2
+
3
+ module SCSSLint
4
+ class Linter::CommentLinter < Linter
5
+ include LinterRegistry
6
+
7
+ def visit_comment(node)
8
+ add_lint(node) unless node.invisible?
9
+ end
10
+
11
+ def description
12
+ 'Use // comments everywhere'
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,45 @@
1
+ require 'sass'
2
+
3
+ module SCSSLint
4
+ class Linter::DeclaredNameLinter < Linter
5
+ include LinterRegistry
6
+
7
+ def visit_function(node)
8
+ check(node)
9
+ yield # Continue into content block of this function definition
10
+ end
11
+
12
+ def visit_mixin(node)
13
+ check(node)
14
+ yield # Continue into content block of this mixin's block
15
+ end
16
+
17
+ def visit_mixindef(node)
18
+ check(node)
19
+ yield # Continue into content block of this mixin definition
20
+ end
21
+
22
+ def visit_variable(node)
23
+ check(node)
24
+ yield # Continue into expression tree for this variable definition
25
+ end
26
+
27
+ def visit_script_funcall(node)
28
+ check(node)
29
+ end
30
+
31
+ def visit_script_variable(node)
32
+ check(node)
33
+ end
34
+
35
+ def description
36
+ 'Names of variables, functions, and mixins should be lowercase and not contain underscores. Use hyphens instead.'
37
+ end
38
+
39
+ private
40
+
41
+ def check(node)
42
+ add_lint(node) if node.name =~ /[_A-Z]/
43
+ end
44
+ end
45
+ end
@@ -4,10 +4,17 @@ module SCSSLint
4
4
  class Linter::HexLinter < Linter
5
5
  include LinterRegistry
6
6
 
7
- def visit_prop(node)
8
- if node.value.is_a?(Sass::Script::String) &&
9
- node.value.to_s =~ /#(\h{3,6})/
10
- add_lint(node) unless valid_hex?($1)
7
+ def visit_root(node)
8
+ # We can't do this via the parse tree because the parser automatically
9
+ # normalizes all colors encountered in Sass script, so we lose the ability
10
+ # to check the format of the color. Thus we resort to line-by-line regex
11
+ # matching.
12
+ engine.lines.each_with_index do |line, index|
13
+ line.scan /#(\h{3,6})/ do |match|
14
+ unless valid_hex?(match.first)
15
+ @lints << Lint.new(engine.filename, index + 1, description)
16
+ end
17
+ end
11
18
  end
12
19
  end
13
20
 
@@ -6,6 +6,7 @@ module SCSSLint
6
6
 
7
7
  def visit_prop(node)
8
8
  line = engine.lines[node.line - 1] if node.line
9
+
9
10
  add_lint(node) unless line =~ PROPERTY_RE
10
11
  end
11
12
 
@@ -19,11 +20,11 @@ module SCSSLint
19
20
 
20
21
  VALUE_RE = %r{(\S+\s)*\S+} # eg. "10px", "10px normal"
21
22
  PROPERTY_RE = %r{
22
- ^\s*[\w-]+:\s # property name, colon, one space
23
- ( # followed by
24
- #{VALUE_RE}; # property and terminating semi-colon eg. a b c;
25
- | # or
26
- ((#{VALUE_RE}\s)?\{) # nested property, optional value, trailing curly
23
+ ^\s*[^:]+(?<!\s):\s # property name, colon not preceded by a space, one space
24
+ ( # followed by
25
+ #{VALUE_RE}; # property and terminating semi-colon eg. a b c;
26
+ | # or
27
+ ((#{VALUE_RE}\s)?\{) # nested property, optional value, trailing curly
27
28
  )
28
29
  }x
29
30
  end
@@ -5,12 +5,18 @@ module SCSSLint
5
5
  include LinterRegistry
6
6
 
7
7
  def visit_prop(node)
8
- unless SHORTHANDABLE_PROPERTIES.include? node.name.first.to_s
9
- return
10
- end
11
-
12
- if node.value.to_sass.strip =~ /\A(\S+\s+\S+(\s+\S+){0,2})\Z/
13
- add_lint(node) unless valid_shorthand?($1)
8
+ return unless SHORTHANDABLE_PROPERTIES.include? node.name.first.to_s
9
+
10
+ case node.value
11
+ when Sass::Script::List
12
+ items = node.value.children
13
+ if (2..4).member?(items.count)
14
+ add_lint(node) unless valid_shorthand?(*items.map(&:to_sass))
15
+ end
16
+ when Sass::Script::String
17
+ if node.value.to_sass.strip =~ /\A(\S+\s+\S+(\s+\S+){0,2})\Z/
18
+ add_lint(node) unless valid_shorthand?(*$1.split(/\s+/))
19
+ end
14
20
  end
15
21
  end
16
22
 
@@ -27,10 +33,7 @@ module SCSSLint
27
33
  margin
28
34
  padding]
29
35
 
30
- def valid_shorthand?(shorthand)
31
- values = shorthand.split(/\s+/)
32
- top, right, bottom, left = values
33
-
36
+ def valid_shorthand?(top, right, bottom = nil, left = nil)
34
37
  if top == right && right == bottom && bottom == left
35
38
  false
36
39
  elsif top == right && bottom.nil? && left.nil?
@@ -5,12 +5,36 @@ module SCSSLint
5
5
  include LinterRegistry
6
6
 
7
7
  def visit_rule(node)
8
- add_lint(node) unless node.rule.grep(/,[^\n]/).empty?
8
+ add_lint(node) if invalid_comma_placement? node
9
9
  yield # Continue linting children
10
10
  end
11
11
 
12
12
  def description
13
13
  'Each selector should be on its own line'
14
14
  end
15
+
16
+ private
17
+
18
+ # A comma is invalid if it starts the line or is not the end of the line
19
+ def invalid_comma_placement?(node)
20
+ normalize_spacing(condense_to_string(node.rule)) =~ /\n,|,[^\n]/
21
+ end
22
+
23
+ # Since RuleNode.rule returns an array containing both String and
24
+ # Sass::Script::Nodes, we need to condense it into a single string that we
25
+ # can run a regex against.
26
+ def condense_to_string(sequence_list)
27
+ sequence_list.inject('') do |combined, string_or_script|
28
+ combined + (string_or_script.is_a?(String) ? string_or_script
29
+ : string_or_script.to_sass)
30
+ end
31
+ end
32
+
33
+ # Removes extra spacing between lines in a comma-separated sequence due to
34
+ # comments being removed in the parse phase. This makes it easy to check if
35
+ # a comma is where belongs.
36
+ def normalize_spacing(string_sequence)
37
+ string_sequence.gsub(/,[^\S\n]*\n\s*/, ",\n")
38
+ end
15
39
  end
16
40
  end
@@ -21,7 +21,18 @@ module SCSSLint
21
21
 
22
22
  # Helper for creating lint from a parse tree node
23
23
  def add_lint(node)
24
- @lints << Lint.new(node.filename, node.line, description)
24
+ @lints << Lint.new(engine.filename, node.line, description)
25
+ end
26
+
27
+ # Monkey-patched implementation that adds support for traversing
28
+ # Sass::Script::Nodes (original implementation only supports
29
+ # Sass::Tree::Nodes).
30
+ def node_name(node)
31
+ if node.is_a?(Sass::Script::Node)
32
+ "script_#{node.class.name.gsub(/.*::(.*?)$/, '\\1').downcase}"
33
+ else
34
+ super
35
+ end
25
36
  end
26
37
  end
27
38
  end
@@ -1,3 +1,3 @@
1
1
  module SCSSLint
2
- VERSION = '0.6.7'
2
+ VERSION = '0.7.0'
3
3
  end
data/lib/scss_lint.rb CHANGED
@@ -10,6 +10,10 @@ module SCSSLint
10
10
  autoload :Runner, 'scss_lint/runner'
11
11
  autoload :VERSION, 'scss_lint/version'
12
12
 
13
+ # Preload Sass so we can monkey patch it
14
+ require 'sass'
15
+ require 'sass/tree'
16
+
13
17
  # Load all linters
14
18
  Dir[File.expand_path('scss_lint/linter/*.rb', File.dirname(__FILE__))].each do |file|
15
19
  require file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scss-lint
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.7
4
+ version: 0.7.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-05-10 00:00:00.000000000 Z
12
+ date: 2013-06-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: colorize
@@ -84,13 +84,17 @@ executables:
84
84
  extensions: []
85
85
  extra_rdoc_files: []
86
86
  files:
87
+ - lib/sass/tree.rb
87
88
  - lib/scss_lint.rb
88
89
  - lib/scss_lint/cli.rb
89
90
  - lib/scss_lint/engine.rb
90
91
  - lib/scss_lint/lint.rb
91
92
  - lib/scss_lint/linter.rb
93
+ - lib/scss_lint/linter/border_zero_linter.rb
94
+ - lib/scss_lint/linter/comment_linter.rb
92
95
  - lib/scss_lint/linter/debug_linter.rb
93
96
  - lib/scss_lint/linter/declaration_order_linter.rb
97
+ - lib/scss_lint/linter/declared_name_linter.rb
94
98
  - lib/scss_lint/linter/empty_rule_linter.rb
95
99
  - lib/scss_lint/linter/hex_linter.rb
96
100
  - lib/scss_lint/linter/property_format_linter.rb
@@ -107,7 +111,8 @@ files:
107
111
  - lib/scss_lint/version.rb
108
112
  - bin/scss-lint
109
113
  homepage: http://github.com/causes/scss-lint
110
- licenses: []
114
+ licenses:
115
+ - MIT
111
116
  post_install_message:
112
117
  rdoc_options: []
113
118
  require_paths: