-
Notifications
You must be signed in to change notification settings - Fork 103
/
utils.rb
286 lines (247 loc) · 9.69 KB
/
utils.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
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
# frozen_string_literal: true
require 'pathname'
module HamlLint
# A miscellaneous set of utility functions.
module Utils # rubocop:disable Metrics/ModuleLength
module_function
# Returns whether a glob pattern (or any of a list of patterns) matches the
# specified file.
#
# This is defined here so our file globbing options are consistent
# everywhere we perform globbing.
#
# @param glob [String, Array]
# @param file [String]
# @return [Boolean]
def any_glob_matches?(globs_or_glob, file)
get_abs_and_rel_path(file).any? do |path|
Array(globs_or_glob).any? do |glob|
::File.fnmatch?(glob, path,
::File::FNM_PATHNAME | # Wildcards don't match path separators
::File::FNM_DOTMATCH) # `*` wildcard matches dotfiles
end
end
end
# Returns an array of two items, the first being the absolute path, the second
# the relative path.
#
# The relative path is relative to the current working dir. The path passed can
# be either relative or absolute.
#
# @param path [String] Path to get absolute and relative path of
# @return [Array<String>] Absolute and relative path
def get_abs_and_rel_path(path)
original_path = Pathname.new(path)
root_dir_path = Pathname.new(File.expand_path(Dir.pwd))
if original_path.absolute?
[path, original_path.relative_path_from(root_dir_path)]
else
[root_dir_path + original_path, path]
end
end
# Yields interpolated values within a block of text.
#
# @param text [String]
# @yield Passes interpolated code and line number that code appears on in
# the text.
# @yieldparam interpolated_code [String] code that was interpolated
# @yieldparam line [Integer] line number code appears on in text
def extract_interpolated_values(text) # rubocop:disable Metrics/AbcSize
dumped_text = text.dump
# Basically, match pairs of '\' and '\ followed by the letter 'n'
quoted_regex_s = "(#{Regexp.quote('\\\\')}|#{Regexp.quote('\\n')})"
newline_positions = extract_substring_positions(dumped_text, quoted_regex_s)
# Filter the matches to only keep those ending in 'n'.
# This way, escaped \n will not be considered
newline_positions.select! do |pos|
dumped_text[pos - 1] == 'n'
end
Haml::Util.handle_interpolation(dumped_text) do |scan|
line = (newline_positions.find_index { |marker| scan.charpos <= marker } ||
newline_positions.size) + 1
escape_count = (scan[2].size - 1) / 2
break unless escape_count.even?
dumped_interpolated_str = Haml::Util.balance(scan, '{', '}', 1)[0][0...-1]
# Hacky way to turn a dumped string back into a regular string
yield [eval('"' + dumped_interpolated_str + '"'), line] # rubocop:disable Security/Eval
end
end
def handle_interpolation_with_indexes(text)
newline_indexes = extract_substring_positions(text, "\n")
handle_interpolation_with_newline(text) do |scan|
line_index = newline_indexes.find_index { |index| scan.charpos <= index }
line_index ||= newline_indexes.size
line_start_char_index = if line_index == 0
0
else
newline_indexes[line_index - 1]
end
char_index = scan.charpos - line_start_char_index
yield scan, line_index, char_index
end
end
if Gem::Version.new(Haml::VERSION) >= Gem::Version.new('5')
# Same as Haml::Util.handle_interpolation, but enables multiline mode on the regex
def handle_interpolation_with_newline(str)
scan = StringScanner.new(str)
yield scan while scan.scan(/(.*?)(\\*)#([{@$])/m)
scan.rest
end
else
# Same as Haml::Util.handle_interpolation, but enables multiline mode on the regex
def handle_interpolation_with_newline(str)
scan = StringScanner.new(str)
yield scan while scan.scan(/(.*?)(\\*)\#\{/m)
scan.rest
end
end
# Returns indexes of all occurrences of a substring within a string.
#
# Note, this will not return overlapping substrings, so searching for "aa"
# in "aaa" will only find one substring, not two.
#
# @param text [String] the text to search
# @param substr [String] the substring to search for
# @return [Array<Integer>] list of indexes where the substring occurs
def extract_substring_positions(text, substr)
positions = []
scanner = StringScanner.new(text)
positions << scanner.charpos while scanner.scan(/(.*?)#{substr}/)
positions
end
# Converts a string containing underscores/hyphens/spaces into CamelCase.
#
# @param str [String]
# @return [String]
def camel_case(str)
str.split(/_|-| /).map { |part| part.sub(/^\w/, &:upcase) }.join
end
# Find all consecutive items satisfying the given block of a minimum size,
# yielding each group of consecutive items to the provided block.
#
# @param items [Array]
# @param satisfies [Proc] function that takes an item and returns true/false
# @param min_consecutive [Fixnum] minimum number of consecutive items before
# yielding the group
# @yield Passes list of consecutive items all matching the criteria defined
# by the `satisfies` {Proc} to the provided block
# @yieldparam group [Array] List of consecutive items
# @yieldreturn [Boolean] block should return whether item matches criteria
# for inclusion
def for_consecutive_items(items, satisfies, min_consecutive = 2)
current_index = -1
while (current_index += 1) < items.count
next unless satisfies[items[current_index]]
count = count_consecutive(items, current_index, &satisfies)
next unless count >= min_consecutive
# Yield the chunk of consecutive items
yield items[current_index...(current_index + count)]
current_index += count # Skip this patch of consecutive items to find more
end
end
# Count the number of consecutive items satisfying the given {Proc}.
#
# @param items [Array]
# @param offset [Fixnum] index to start searching from
# @yield [item] Passes item to the provided block.
# @yieldparam item [Object] Item to evaluate as matching criteria for
# inclusion
# @yieldreturn [Boolean] whether to include the item
# @return [Integer]
def count_consecutive(items, offset = 0)
count = 1
count += 1 while (offset + count < items.count) && yield(items[offset + count])
count
end
# Process ERB, providing some values for for versions to it
#
# @param content [String] the (usually yaml) content to process
# @return [String]
def process_erb(content)
# Variables for use in the ERB's post-processing
rubocop_version = HamlLint::VersionComparer.for_rubocop
ERB.new(content).result(binding)
end
def insert_after_indentation(code, insert)
index = code.index(/\S/)
"#{code[0...index]}#{insert}#{code[index..]}"
end
# Calls a block of code with a modified set of environment variables,
# restoring them once the code has executed.
#
# @param env [Hash] environment variables to set
def with_environment(env)
old_env = {}
env.each do |var, value|
old_env[var] = ENV[var.to_s]
ENV[var.to_s] = value
end
yield
ensure
old_env.each { |var, value| ENV[var.to_s] = value }
end
def indent(string, nb_indent)
if nb_indent < 0
string.gsub(/^ {1,#{-nb_indent}}/, '')
else
string.gsub(/^/, ' ' * nb_indent)
end
end
def map_subset!(array, range, &block)
subset = array[range]
return if subset.nil? || subset.empty?
array[range] = subset.map(&block)
end
def map_after_first!(array, &block)
map_subset!(array, 1..-1, &block)
end
# Returns true if line is only whitespace.
# Note, this is not like blank? is rails. For nil, this returns false.
def is_blank_line?(line)
line && line.index(/\S/).nil?
end
def check_error_when_compiling_haml(haml_string)
begin
ruby_code = ::HamlLint::Adapter.detect_class.new(haml_string).precompile
rescue StandardError => e
return e
end
eval("BEGIN {return nil}; #{ruby_code}", binding, __FILE__, __LINE__) # rubocop:disable Security/Eval
# The eval will return nil
rescue ::SyntaxError
$!
end
# Overrides the global stdin, stdout and stderr while within the block, to
# push a string in stdin, and capture both stdout and stderr which are returned.
#
# @param stdin_str [String] the string to push in as stdin
# @param _block [Block] the block to perform with the overridden std streams
# @return [String, String]
def with_captured_streams(stdin_str, &_block)
original_stdin = $stdin
# The dup is needed so that stdin_data isn't altered (encoding-wise at least)
$stdin = StringIO.new(stdin_str.dup)
begin
original_stdout = $stdout
$stdout = StringIO.new
begin
original_stderr = $stderr
$stderr = StringIO.new
yield
[$stdout.string, $stderr.string]
ensure
$stderr = original_stderr
end
ensure
$stdout = original_stdout
end
ensure
$stdin = original_stdin
end
def regexp_for_parts(parts, join_regexp, prefix: nil, suffix: nil)
regexp_code = parts.map { |c| Regexp.quote(c) }.join(join_regexp)
regexp_code = "#{prefix}#{regexp_code}#{suffix}"
Regexp.new(regexp_code)
end
end
end