forked from sds/scss-lint
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlinter.rb
215 lines (184 loc) · 7.39 KB
/
linter.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
module SCSSLint
# Defines common functionality available to all linters.
class Linter < Sass::Tree::Visitors::Base # rubocop:disable ClassLength
include SelectorVisitor
include Utils
class << self
attr_accessor :simple_name
# When defining a Linter class, define its simple name as well. This
# assumes that the module hierarchy of every linter starts with
# `SCSSLint::Linter::`, and removes this part of the class name.
#
# `SCSSLint::Linter::Foo.simple_name` #=> "Foo"
# `SCSSLint::Linter::Compass::Bar.simple_name` #=> "Compass::Bar"
def inherited(linter)
name_parts = linter.name.split('::')
name = name_parts.length < 3 ? '' : name_parts[2..-1].join('::')
linter.simple_name = name
end
end
attr_reader :config, :engine, :lints
# Create a linter.
def initialize
@lints = []
end
# Run this linter against a parsed document with the given configuration,
# returning the lints that were found.
#
# @param engine [Engine]
# @param config [Config]
# @return [Array<Lint>]
def run(engine, config)
@lints = []
@config = config
@engine = engine
@comment_processor = ControlCommentProcessor.new(self)
visit(engine.tree)
@lints = @comment_processor.filter_lints(@lints)
end
# Return the human-friendly name of this linter as specified in the
# configuration file and in lint descriptions.
def name
self.class.simple_name
end
protected
# Helper for creating lint from a parse tree node
#
# @param node_or_line_or_location [Sass::Script::Tree::Node, Fixnum,
# SCSSLint::Location, Sass::Source::Position]
# @param message [String]
def add_lint(node_or_line_or_location, message)
@lints << Lint.new(self,
engine.filename,
extract_location(node_or_line_or_location),
message,
@config.fetch('severity', :warning).to_sym)
end
# Extract {SCSSLint::Location} from a {Sass::Source::Range}.
#
# @param range [Sass::Source::Range]
# @return [SCSSLint::Location]
def location_from_range(range) # rubocop:disable Metrics/AbcSize
length = if range.start_pos.line == range.end_pos.line
range.end_pos.offset - range.start_pos.offset
else
line_source = engine.lines[range.start_pos.line - 1]
line_source.length - range.start_pos.offset + 1
end
# Workaround for https://github.com/brigade/scss-lint/issues/887 to acount for
# https://github.com/sass/sass/issues/2284.
length = 1 if length < 1
Location.new(range.start_pos.line, range.start_pos.offset, length)
end
# Extracts the original source code given a range.
#
# @param source_range [Sass::Source::Range]
# @return [String] the original source code
def source_from_range(source_range) # rubocop:disable Metrics/AbcSize
current_line = source_range.start_pos.line - 1
last_line = source_range.end_pos.line - 1
start_pos = source_range.start_pos.offset - 1
source =
if current_line == last_line
engine.lines[current_line][start_pos..(source_range.end_pos.offset - 1)]
else
engine.lines[current_line][start_pos..-1]
end
current_line += 1
while current_line < last_line
source += engine.lines[current_line].to_s
current_line += 1
end
if source_range.start_pos.line != source_range.end_pos.line
source += ((engine.lines[current_line] || '')[0...source_range.end_pos.offset]).to_s
end
source
end
# Returns whether a given node spans only a single line.
#
# @param node [Sass::Tree::Node]
# @return [true,false] whether the node spans a single line
def node_on_single_line?(node)
return if node.source_range.start_pos.line != node.source_range.end_pos.line
# The Sass parser reports an incorrect source range if the trailing curly
# brace is on the next line, e.g.
#
# p {
# }
#
# Since we don't want to count this as a single line node, check if the
# last character on the first line is an opening curly brace.
engine.lines[node.line - 1].strip[-1] != '{'
end
# Modified so we can also visit selectors in linters
#
# @param node [Sass::Tree::Node, Sass::Script::Tree::Node,
# Sass::Script::Value::Base]
def visit(node)
# Visit the selector of a rule if parsed rules are available
if node.is_a?(Sass::Tree::RuleNode) && node.parsed_rules
visit_selector(node.parsed_rules)
end
@comment_processor.before_node_visit(node) if @engine.any_control_commands
super
@comment_processor.after_node_visit(node) if @engine.any_control_commands
end
# Redefine so we can set the `node_parent` of each node
#
# @param parent [Sass::Tree::Node, Sass::Script::Tree::Node,
# Sass::Script::Value::Base]
def visit_children(parent)
parent.children.each do |child|
child.node_parent = parent
visit(child)
end
end
private
def visit_comment(_node)
# Don't lint children of comments by default, as the Sass parser contains
# many bugs related to the source ranges reported within code in /*...*/
# comments.
#
# Instead of defining this empty method on every linter, we assume every
# linter ignores comments by default. Individual linters can override at
# their discretion.
end
def extract_location(node_or_line_or_location)
if node_or_line_or_location.is_a?(Location)
node_or_line_or_location
elsif node_or_line_or_location.is_a?(Sass::Source::Position)
Location.new(node_or_line_or_location.line, node_or_line_or_location.offset)
elsif node_or_line_or_location.respond_to?(:source_range) &&
node_or_line_or_location.source_range
location_from_range(node_or_line_or_location.source_range)
elsif node_or_line_or_location.respond_to?(:line)
Location.new(node_or_line_or_location.line)
else
Location.new(node_or_line_or_location)
end
end
# @param source_position [Sass::Source::Position]
# @param offset [Integer]
# @return [String] the character at the given [Sass::Source::Position]
def character_at(source_position, offset = 0)
actual_line = source_position.line - 1
actual_offset = source_position.offset + offset - 1
return nil if actual_offset < 0
engine.lines.size > actual_line && engine.lines[actual_line][actual_offset]
end
# Starting at source_position (plus offset), search for pattern and return
# the offset from the source_position.
#
# @param source_position [Sass::Source::Position]
# @param pattern [String, RegExp] the pattern to search for
# @param offset [Integer]
# @return [Integer] the offset at which [pattern] was found.
def offset_to(source_position, pattern, offset = 0)
actual_line = source_position.line - 1
actual_offset = source_position.offset + offset - 1
return nil if actual_line >= engine.lines.size
actual_index = engine.lines[actual_line].index(pattern, actual_offset)
actual_index && actual_index + 1 - source_position.offset
end
end
end