foodcritic 15.1.0 → 16.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +2 -2
- data/chef_dsl_metadata/{chef_14.8.12.json → chef_14.14.29.json} +394 -169
- data/chef_dsl_metadata/{chef_13.12.3.json → chef_15.4.45.json} +15454 -2631
- data/foodcritic.gemspec +2 -2
- data/lib/foodcritic.rb +2 -1
- data/lib/foodcritic/api.rb +55 -42
- data/lib/foodcritic/chef.rb +5 -3
- data/lib/foodcritic/command_line.rb +78 -52
- data/lib/foodcritic/domain.rb +7 -9
- data/lib/foodcritic/dsl.rb +4 -1
- data/lib/foodcritic/gerkin/tag.rb +55 -0
- data/lib/foodcritic/gerkin/tag_expression.rb +66 -0
- data/lib/foodcritic/linter.rb +24 -8
- data/lib/foodcritic/notifications.rb +3 -1
- data/lib/foodcritic/output.rb +1 -1
- data/lib/foodcritic/rules/fc001.rb +6 -6
- data/lib/foodcritic/rules/fc004.rb +1 -0
- data/lib/foodcritic/rules/fc006.rb +9 -9
- data/lib/foodcritic/rules/fc007.rb +3 -1
- data/lib/foodcritic/rules/fc016.rb +1 -0
- data/lib/foodcritic/rules/fc019.rb +7 -6
- data/lib/foodcritic/rules/fc022.rb +24 -25
- data/lib/foodcritic/rules/fc024.rb +16 -13
- data/lib/foodcritic/rules/fc029.rb +1 -0
- data/lib/foodcritic/rules/fc031.rb +1 -1
- data/lib/foodcritic/rules/fc032.rb +2 -2
- data/lib/foodcritic/rules/fc033.rb +2 -2
- data/lib/foodcritic/rules/fc034.rb +5 -2
- data/lib/foodcritic/rules/fc039.rb +12 -12
- data/lib/foodcritic/rules/fc040.rb +1 -1
- data/lib/foodcritic/rules/fc044.rb +8 -12
- data/lib/foodcritic/rules/fc048.rb +1 -0
- data/lib/foodcritic/rules/fc121.rb +6 -10
- data/lib/foodcritic/rules/fc123.rb +16 -0
- data/lib/foodcritic/template.rb +3 -6
- data/lib/foodcritic/version.rb +1 -1
- metadata +34 -32
data/lib/foodcritic/domain.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
require "cucumber/core/gherkin/tag_expression"
|
2
|
-
|
3
1
|
module FoodCritic
|
4
2
|
# A warning of a possible issue
|
5
3
|
class Warning
|
@@ -34,7 +32,7 @@ module FoodCritic
|
|
34
32
|
|
35
33
|
def to_s
|
36
34
|
@rules.sort_by(&:code)
|
37
|
-
.map
|
35
|
+
.map(&:to_s).join("\n")
|
38
36
|
end
|
39
37
|
|
40
38
|
end
|
@@ -50,12 +48,12 @@ module FoodCritic
|
|
50
48
|
|
51
49
|
# If any of the warnings in this review have failed or not.
|
52
50
|
def failed?
|
53
|
-
warnings.any?
|
51
|
+
warnings.any?(&:failed?)
|
54
52
|
end
|
55
53
|
|
56
54
|
# Returns an array of warnings that are marked as failed.
|
57
55
|
def failures
|
58
|
-
warnings.select
|
56
|
+
warnings.select(&:failed?)
|
59
57
|
end
|
60
58
|
|
61
59
|
# Returns a string representation of this review. This representation is
|
@@ -76,8 +74,8 @@ module FoodCritic
|
|
76
74
|
# A rule to be matched against.
|
77
75
|
class Rule
|
78
76
|
attr_accessor :code, :name, :applies_to, :cookbook, :attributes, :recipe,
|
79
|
-
|
80
|
-
|
77
|
+
:provider, :resource, :metadata, :library, :template, :role,
|
78
|
+
:environment
|
81
79
|
|
82
80
|
attr_writer :tags
|
83
81
|
|
@@ -95,8 +93,8 @@ module FoodCritic
|
|
95
93
|
|
96
94
|
# Checks the rule tags to see if they match a Gherkin (Cucumber) expression
|
97
95
|
def matches_tags?(tag_expr)
|
98
|
-
|
99
|
-
tags.map { |tag|
|
96
|
+
::Foodcritic::Gherkin::TagExpression.new(tag_expr).evaluate(
|
97
|
+
tags.map { |tag| ::Foodcritic::Gherkin::Tag.new(nil, tag) }
|
100
98
|
)
|
101
99
|
end
|
102
100
|
|
data/lib/foodcritic/dsl.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require "open-uri"
|
1
2
|
require "pathname"
|
2
3
|
|
3
4
|
module FoodCritic
|
@@ -76,7 +77,9 @@ module FoodCritic
|
|
76
77
|
paths.map do |path|
|
77
78
|
File.directory?(path) ? Dir["#{path}/**/*.rb"].sort : path
|
78
79
|
end.flatten.each do |path|
|
79
|
-
|
80
|
+
rule_file = open(path)
|
81
|
+
dsl.instance_eval(rule_file.read, path)
|
82
|
+
rule_file.close
|
80
83
|
end
|
81
84
|
dsl.rules
|
82
85
|
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# this is vendored from cucumber-core gem with the module/class names changed
|
2
|
+
|
3
|
+
module Foodcritic
|
4
|
+
module Gherkin
|
5
|
+
class Tag
|
6
|
+
|
7
|
+
attr_reader :name
|
8
|
+
|
9
|
+
def initialize(location, name)
|
10
|
+
@location = location
|
11
|
+
@name = name
|
12
|
+
end
|
13
|
+
|
14
|
+
def inspect
|
15
|
+
%{#<#{self.class} "#{name}" (#{location})>}
|
16
|
+
end
|
17
|
+
|
18
|
+
def file_colon_line
|
19
|
+
location.to_s
|
20
|
+
end
|
21
|
+
|
22
|
+
def file
|
23
|
+
location.file
|
24
|
+
end
|
25
|
+
|
26
|
+
def line
|
27
|
+
location.line
|
28
|
+
end
|
29
|
+
|
30
|
+
def location
|
31
|
+
raise('Please set @location in the constructor') unless defined?(@location)
|
32
|
+
@location
|
33
|
+
end
|
34
|
+
|
35
|
+
def attributes
|
36
|
+
[tags, comments, multiline_arg].flatten
|
37
|
+
end
|
38
|
+
|
39
|
+
def tags
|
40
|
+
# will be overriden by nodes that actually have tags
|
41
|
+
[]
|
42
|
+
end
|
43
|
+
|
44
|
+
def comments
|
45
|
+
# will be overriden by nodes that actually have comments
|
46
|
+
[]
|
47
|
+
end
|
48
|
+
|
49
|
+
def multiline_arg
|
50
|
+
# will be overriden by nodes that actually have a multiline_argument
|
51
|
+
Test::EmptyMultilineArgument.new
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# this is vendored from cucumber-core gem with the module/class names changed
|
2
|
+
# in the 4.0 release of cucumber-core they removed support for legacy tags (~FC100)
|
3
|
+
# and we're not going to force everyone to redo their tags on a EOL product
|
4
|
+
|
5
|
+
module Foodcritic
|
6
|
+
module Gherkin
|
7
|
+
class TagExpression
|
8
|
+
|
9
|
+
attr_reader :limits
|
10
|
+
|
11
|
+
def initialize(tag_expressions)
|
12
|
+
@ands = []
|
13
|
+
@limits = {}
|
14
|
+
tag_expressions.each do |expr|
|
15
|
+
add(expr.strip.split(/\s*,\s*/))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def empty?
|
20
|
+
@ands.empty?
|
21
|
+
end
|
22
|
+
|
23
|
+
def evaluate(tags)
|
24
|
+
return true if @ands.flatten.empty?
|
25
|
+
vars = Hash[*tags.map{|tag| [tag.name, true]}.flatten]
|
26
|
+
raise "No vars" if vars.nil? # Useless statement to prevent ruby warnings about unused var
|
27
|
+
!!Kernel.eval(ruby_expression)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def add(tags_with_negation_and_limits)
|
33
|
+
negatives, positives = tags_with_negation_and_limits.partition{|tag| tag =~ /^~/}
|
34
|
+
@ands << (store_and_extract_limits(negatives, true) + store_and_extract_limits(positives, false))
|
35
|
+
end
|
36
|
+
|
37
|
+
def store_and_extract_limits(tags_with_negation_and_limits, negated)
|
38
|
+
tags_with_negation = []
|
39
|
+
tags_with_negation_and_limits.each do |tag_with_negation_and_limit|
|
40
|
+
tag_with_negation, limit = tag_with_negation_and_limit.split(':')
|
41
|
+
tags_with_negation << tag_with_negation
|
42
|
+
if limit
|
43
|
+
tag_without_negation = negated ? tag_with_negation[1..-1] : tag_with_negation
|
44
|
+
if @limits[tag_without_negation] && @limits[tag_without_negation] != limit.to_i
|
45
|
+
raise "Inconsistent tag limits for #{tag_without_negation}: #{@limits[tag_without_negation]} and #{limit.to_i}"
|
46
|
+
end
|
47
|
+
@limits[tag_without_negation] = limit.to_i
|
48
|
+
end
|
49
|
+
end
|
50
|
+
tags_with_negation
|
51
|
+
end
|
52
|
+
|
53
|
+
def ruby_expression
|
54
|
+
"(" + @ands.map do |ors|
|
55
|
+
ors.map do |tag|
|
56
|
+
if tag =~ /^~(.*)/
|
57
|
+
"!vars['#{$1}']"
|
58
|
+
else
|
59
|
+
"vars['#{tag}']"
|
60
|
+
end
|
61
|
+
end.join("||")
|
62
|
+
end.join(")&&(") + ")"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/foodcritic/linter.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require "open-uri"
|
1
2
|
require "optparse"
|
2
3
|
require "ripper"
|
3
4
|
require "set"
|
@@ -9,7 +10,7 @@ module FoodCritic
|
|
9
10
|
|
10
11
|
# The default version that will be used to determine relevant rules. This
|
11
12
|
# can be over-ridden at the command line with the `--chef-version` option.
|
12
|
-
DEFAULT_CHEF_VERSION = "
|
13
|
+
DEFAULT_CHEF_VERSION = "15.4.45".freeze
|
13
14
|
attr_reader :chef_version
|
14
15
|
|
15
16
|
# Perform a lint check. This method is intended for use by the command-line
|
@@ -19,6 +20,7 @@ module FoodCritic
|
|
19
20
|
# The first item is the string output, the second is exit code.
|
20
21
|
return [cmd_line.help, 0] if cmd_line.show_help?
|
21
22
|
return [cmd_line.version, 0] if cmd_line.show_version?
|
23
|
+
|
22
24
|
if !cmd_line.valid_grammar?
|
23
25
|
[cmd_line.help, 4]
|
24
26
|
elsif cmd_line.list_rules?
|
@@ -68,6 +70,7 @@ module FoodCritic
|
|
68
70
|
options = setup_defaults(options)
|
69
71
|
@options = options
|
70
72
|
@chef_version = options[:chef_version] || DEFAULT_CHEF_VERSION
|
73
|
+
ast_cache(options[:ast_cache_size])
|
71
74
|
|
72
75
|
warnings = []; last_dir = nil; matched_rule_tags = Set.new
|
73
76
|
load_rules
|
@@ -111,8 +114,8 @@ module FoodCritic
|
|
111
114
|
# Convert the matches into warnings
|
112
115
|
matches.each do |match|
|
113
116
|
warnings << Warning.new(state[:rule],
|
114
|
-
|
115
|
-
|
117
|
+
{ filename: state[:file] }.merge(match),
|
118
|
+
options)
|
116
119
|
matched_rule_tags << state[:rule].tags
|
117
120
|
end
|
118
121
|
end
|
@@ -132,16 +135,19 @@ module FoodCritic
|
|
132
135
|
|
133
136
|
if dsl_method_for_file(state[:file])
|
134
137
|
cbk_matches += matches(state[:rule].send(
|
135
|
-
dsl_method_for_file(state[:file])
|
138
|
+
dsl_method_for_file(state[:file])
|
139
|
+
), state[:ast], state[:file])
|
136
140
|
end
|
137
141
|
|
138
142
|
per_cookbook_rules(state[:last_dir], state[:file]) do
|
139
143
|
if File.basename(state[:file]) == "metadata.rb"
|
140
144
|
cbk_matches += matches(
|
141
|
-
state[:rule].metadata, state[:ast], state[:file]
|
145
|
+
state[:rule].metadata, state[:ast], state[:file]
|
146
|
+
)
|
142
147
|
end
|
143
148
|
cbk_matches += matches(
|
144
|
-
state[:rule].cookbook, cookbook_dir(state[:file])
|
149
|
+
state[:rule].cookbook, cookbook_dir(state[:file])
|
150
|
+
)
|
145
151
|
end
|
146
152
|
|
147
153
|
cbk_matches
|
@@ -196,6 +202,7 @@ module FoodCritic
|
|
196
202
|
# Some rules are version specific.
|
197
203
|
def applies_to_version?(rule, version)
|
198
204
|
return true unless version
|
205
|
+
|
199
206
|
rule.applies_to.yield(Gem::Version.create(version))
|
200
207
|
end
|
201
208
|
|
@@ -216,7 +223,8 @@ module FoodCritic
|
|
216
223
|
|
217
224
|
# if a rule file has been specified use that. Otherwise use the .foodcritic file in the CB
|
218
225
|
tags = if @options[:rule_file]
|
219
|
-
raise "ERROR: Could not find the specified rule file at #{@options[:rule_file]}"
|
226
|
+
raise "ERROR: Could not find the specified rule file at #{@options[:rule_file]}" if is_local_file?(@options[:rule_file]) && ! File.exist?(@options[:rule_file])
|
227
|
+
|
220
228
|
parse_rule_file(@options[:rule_file])
|
221
229
|
else
|
222
230
|
File.exist?("#{cookbook}/.foodcritic") ? parse_rule_file("#{cookbook}/.foodcritic") : []
|
@@ -233,7 +241,9 @@ module FoodCritic
|
|
233
241
|
def parse_rule_file(file)
|
234
242
|
tags = []
|
235
243
|
begin
|
236
|
-
|
244
|
+
rule_file = open(file)
|
245
|
+
tag_text = rule_file.read
|
246
|
+
rule_file.close
|
237
247
|
tags = tag_text.split(/\s/)
|
238
248
|
rescue
|
239
249
|
raise "ERROR: Could not read or parse the specified rule file at #{file}"
|
@@ -338,9 +348,15 @@ module FoodCritic
|
|
338
348
|
end.flatten
|
339
349
|
end
|
340
350
|
|
351
|
+
# Determine if the rule file name is a local file or a url
|
352
|
+
def is_local_file?(file_name)
|
353
|
+
! /^(ftps*:|https*:|file:)/.match(file_name)
|
354
|
+
end
|
355
|
+
|
341
356
|
# Invoke the DSL method with the provided parameters.
|
342
357
|
def matches(match_method, *params)
|
343
358
|
return [] unless match_method.respond_to?(:yield)
|
359
|
+
|
344
360
|
matches = match_method.yield(*params)
|
345
361
|
return [] unless matches.respond_to?(:each)
|
346
362
|
|
@@ -133,13 +133,15 @@ module FoodCritic
|
|
133
133
|
is_variable = true unless notify.xpath("args_add_block/args_add//args_add[aref or vcall or call or var_ref]").empty?
|
134
134
|
string_val = notify.xpath("descendant::args_add/string_literal/string_add/tstring_content/@value").first
|
135
135
|
symbol_val = notify.xpath('descendant::args_add/args_add//symbol/ident/@value |
|
136
|
-
descendant::dyna_symbol[1]/xstring_add/tstring_content/@value
|
136
|
+
descendant::dyna_symbol[1]/xstring_add/tstring_content/@value |
|
137
|
+
descendant::args_add/args_add//dyna_symbol/string_add/tstring_content/@value').first
|
137
138
|
|
138
139
|
# 1) return a nil if the action is a variable like node['foo']['bar']
|
139
140
|
# 2) return the symbol if it exists
|
140
141
|
# 3) return the string since we're positive that we're not a symbol or variable
|
141
142
|
return nil if is_variable
|
142
143
|
return symbol_val.value.to_sym unless symbol_val.nil?
|
144
|
+
|
143
145
|
string_val.value
|
144
146
|
end
|
145
147
|
|
data/lib/foodcritic/output.rb
CHANGED
@@ -128,7 +128,7 @@ module FoodCritic
|
|
128
128
|
fmt << 40 + colors.index(bg.to_s) if bg
|
129
129
|
fmt << attrs.index(attr.to_s) if attr
|
130
130
|
if fmt
|
131
|
-
puts "#{escape % fmt.join(
|
131
|
+
puts "#{escape % fmt.join(";")}#{text}#{escape % 0}"
|
132
132
|
else
|
133
133
|
puts text
|
134
134
|
end
|
@@ -1,8 +1,8 @@
|
|
1
1
|
rule "FC001",
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
2
|
+
"Use strings in preference to symbols to access node attributes" do
|
3
|
+
tags %w{style attributes}
|
4
|
+
recipe do |ast|
|
5
|
+
# node.run_state is not actually an attribute so ignore that
|
6
|
+
attribute_access(ast, type: :symbol, ignore: "run_state")
|
7
|
+
end
|
7
8
|
end
|
8
|
-
end
|
@@ -4,6 +4,7 @@ rule "FC004", "Use a service resource to start and stop services" do
|
|
4
4
|
find_resources(ast, type: "execute").find_all do |cmd|
|
5
5
|
cmd_str = (resource_attribute(cmd, "command") || resource_name(cmd)).to_s
|
6
6
|
next if cmd_str.include?(".exe") # don't catch windows commands
|
7
|
+
|
7
8
|
cmd_str.start_with?( "start ", "stop ", "reload ", "restart ") || # upstart jobs
|
8
9
|
( [ "/etc/init.d", "service ", "/sbin/service ", "invoke-rc.d ", "systemctl "].any? do |service_cmd| # upstart / sys-v / systemd
|
9
10
|
cmd_str.start_with?(service_cmd)
|
@@ -1,11 +1,11 @@
|
|
1
1
|
rule "FC006", "Mode should be quoted or fully specified when "\
|
2
2
|
"setting file permissions" do
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
end
|
3
|
+
tags %w{correctness files}
|
4
|
+
recipe do |ast|
|
5
|
+
ast.xpath(%q{//ident[@value='mode']/parent::command/
|
6
|
+
descendant::int[string-length(@value) < 5
|
7
|
+
and not(starts-with(@value, "0")
|
8
|
+
and string-length(@value) = 4)][count(ancestor::aref) = 0]/
|
9
|
+
ancestor::method_add_block})
|
10
|
+
end
|
11
|
+
end
|
@@ -2,8 +2,10 @@ rule "FC007", "Ensure recipe dependencies are reflected in cookbook metadata" do
|
|
2
2
|
tags %w{correctness metadata}
|
3
3
|
recipe do |ast, filename|
|
4
4
|
metadata_path = Pathname.new(
|
5
|
-
File.join(File.dirname(filename), "..", "metadata.rb")
|
5
|
+
File.join(File.dirname(filename), "..", "metadata.rb")
|
6
|
+
).cleanpath
|
6
7
|
next unless File.exist? metadata_path
|
8
|
+
|
7
9
|
actual_included = included_recipes(ast, with_partial_names: false)
|
8
10
|
undeclared = actual_included.keys.map do |recipe|
|
9
11
|
recipe.split("::").first unless recipe =~ /^::/ # skip shorthand included recipes. They're local
|
@@ -3,6 +3,7 @@ rule "FC016", "LWRP does not declare a default action" do
|
|
3
3
|
resource do |ast, filename|
|
4
4
|
# See if we're in a custom resource not an LWRP. Only LWRPs need the default_action
|
5
5
|
next if ast.xpath("//ident/@value='action'")
|
6
|
+
|
6
7
|
unless ["//ident/@value='default_action'",
|
7
8
|
"//def/bodystmt/descendant::assign/
|
8
9
|
var_field/ivar/@value='@action'"].any? { |expr| ast.xpath(expr) }
|
@@ -3,20 +3,21 @@ rule "FC019", "Access node attributes in a consistent manner" do
|
|
3
3
|
cookbook do |cookbook_dir|
|
4
4
|
asts = {}; files = Dir["#{cookbook_dir}/*/*.rb"].reject do |file|
|
5
5
|
relative_path = Pathname.new(file).relative_path_from(
|
6
|
-
Pathname.new(cookbook_dir)
|
6
|
+
Pathname.new(cookbook_dir)
|
7
|
+
)
|
7
8
|
relative_path.to_s.split(File::SEPARATOR).include?("spec")
|
8
9
|
end.map do |file|
|
9
10
|
{ path: file, ast: read_ast(file) }
|
10
11
|
end
|
11
|
-
types =
|
12
|
+
types = %i{string symbol vivified}.map do |type|
|
12
13
|
{
|
13
14
|
access_type: type, count: files.map do |file|
|
14
15
|
attribute_access(file[:ast], type: type, ignore_calls: true,
|
15
16
|
cookbook_dir: cookbook_dir, ignore: "run_state").tap do |ast|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
17
|
+
unless ast.empty?
|
18
|
+
(asts[type] ||= []) << { ast: ast, path: file[:path] }
|
19
|
+
end
|
20
|
+
end.size
|
20
21
|
end.inject(:+)
|
21
22
|
}
|
22
23
|
end.reject { |type| type[:count] == 0 }
|
@@ -4,30 +4,29 @@ rule "FC022", "Resource condition within loop may not behave as expected" do
|
|
4
4
|
ast.xpath("//call[ident/@value='each']/../do_block[count(ancestor::
|
5
5
|
method_add_block/method_add_arg/fcall/ident[@value='only_if' or
|
6
6
|
@value = 'not_if']) = 0]").map do |lp|
|
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
|
-
end.flatten.compact
|
7
|
+
block_vars = lp.xpath("block_var/params/child::*").map do |n|
|
8
|
+
n.name.sub(/^ident/, "")
|
9
|
+
end + lp.xpath("block_var/params/child::*/descendant::ident").map do |v|
|
10
|
+
v["value"]
|
11
|
+
end
|
12
|
+
find_resources(lp).map do |resource|
|
13
|
+
# if any of the parameters to the block are used in a condition then we
|
14
|
+
# have a match
|
15
|
+
unless (block_vars &
|
16
|
+
(resource.xpath(%q{descendant::ident[@value='not_if' or
|
17
|
+
@value='only_if']/ancestor::*[self::method_add_block or
|
18
|
+
self::command][1]/descendant::ident/@value}).map(&:value))).empty?
|
19
|
+
c = resource.xpath("command[count(descendant::string_embexpr) = 0]")
|
20
|
+
if resource.xpath("command/ident/@value").first.value == "define"
|
21
|
+
next
|
22
|
+
end
|
23
|
+
|
24
|
+
resource unless c.empty? || block_vars.any? do |var|
|
25
|
+
!resource.xpath(%Q{command/args_add_block/args_add/
|
26
|
+
var_ref/ident[@value='#{var}']}).empty?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end.flatten.compact
|
32
31
|
end
|
33
32
|
end
|