forked from sds/scss-lint
-
Notifications
You must be signed in to change notification settings - Fork 0
/
private_naming_convention.rb
155 lines (127 loc) · 4.58 KB
/
private_naming_convention.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
module SCSSLint
# Verifies that variables, functions, and mixins that follow the private
# naming convention are defined and used within the same file.
class Linter::PrivateNamingConvention < Linter # rubocop:disable ClassLength
include LinterRegistry
DEFINITIONS = {
Sass::Tree::FunctionNode => {
defines: Sass::Script::Tree::Funcall,
human_name: 'function',
},
Sass::Tree::MixinDefNode => {
defines: Sass::Tree::MixinNode,
human_name: 'mixin',
},
Sass::Tree::VariableNode => {
defines: Sass::Script::Tree::Variable,
human_name: 'variable',
},
}.freeze
HUMAN_NODE_NAMES = Hash[DEFINITIONS.map { |k, v| [k, v[:human_name]] }].freeze
DEFINED_BYS = Hash[DEFINITIONS.map { |k, v| [v[:defines], k] }].freeze
def visit_root(node)
# Register all top-level function, mixin, and variable definitions.
node.children.each_with_object([]) do |child_node|
if DEFINITIONS.key?(child_node.class)
register_node child_node
end
yield
end
# After we have visited everything, we want to see if any private things
# were defined but not used.
after_visit_all
end
def visit_script_funcall(node)
check_privacy(node)
yield # Continue linting any arguments of this function call
end
def visit_mixin(node)
check_privacy(node)
yield # Continue into content block of this mixin's block
end
def visit_script_variable(node)
check_privacy(node)
end
private
def register_node(node, node_text = node.name)
return unless private?(node)
@private_definitions ||= {}
@private_definitions[node.class] ||= {}
@private_definitions[node.class][node_text] = {
node: node,
times_used: 0,
}
end
def check_privacy(node, node_text = node.name)
return unless private?(node)
defined_by_class = defined_by(node)
# Look at top-level private definitions
if @private_definitions &&
@private_definitions[defined_by_class] &&
@private_definitions[defined_by_class][node_text]
@private_definitions[defined_by_class][node_text][:times_used] += 1
return
end
# We did not find a top-level private definition, so let's traverse up the
# tree, looking for private definitions of this node that are scoped.
looking_for = {
node: node,
defined_by: defined_by_class,
location: location_from_range(node.source_range),
}
return if node_defined_earlier_in_branch?(node.node_parent, looking_for)
node_type = humanize_node_class(node)
add_lint(
node,
"Private #{node_type} #{node_text} must be defined in the same file it is used"
)
end
def node_defined_earlier_in_branch?(node_to_look_in, looking_for)
# Look at all of the children of this node and return true if we find a
# defining node that matches in name and type.
node_to_look_in.children.each_with_object([]) do |child_node|
break unless before?(child_node, looking_for[:location])
next unless child_node.class == looking_for[:defined_by]
next unless child_node.name == looking_for[:node].name
return true # We found a match, so we are done
end
return false unless node_to_look_in.node_parent
return unless node_to_look_in.node_parent
# We did not find a match yet, and haven't reached the top of the branch,
# so recurse.
node_defined_earlier_in_branch?(node_to_look_in.node_parent, looking_for)
end
def private?(node)
node.name.start_with?(config['prefix'])
end
def before?(node, before_location)
return true unless node.source_range
location = location_from_range(node.source_range)
return true if location.line < before_location.line
if location.line == before_location.line &&
location.column < before_location.column
return true
end
false
end
def after_visit_all
return unless @private_definitions
@private_definitions.each do |_, nodes|
nodes.each do |node_text, node_info|
next if node_info[:times_used] > 0
node_type = humanize_node_class(node_info[:node])
add_lint(
node_info[:node],
"Private #{node_type} #{node_text} must be used in the same file it is defined"
)
end
end
end
def humanize_node_class(node)
HUMAN_NODE_NAMES[node.class]
end
def defined_by(node)
DEFINED_BYS[node.class]
end
end
end