foodcritic 2.2.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +15 -0
  2. data/CHANGELOG.md +83 -0
  3. data/chef_dsl_metadata/chef_0.10.0.json +2 -3
  4. data/chef_dsl_metadata/chef_0.10.10.json +2 -3
  5. data/chef_dsl_metadata/chef_0.10.2.json +2 -3
  6. data/chef_dsl_metadata/chef_0.10.4.json +2 -3
  7. data/chef_dsl_metadata/chef_0.10.6.json +2 -3
  8. data/chef_dsl_metadata/chef_0.10.8.json +2 -3
  9. data/chef_dsl_metadata/chef_0.8.14.json +2 -3
  10. data/chef_dsl_metadata/chef_0.8.16.json +2 -3
  11. data/chef_dsl_metadata/chef_0.9.0.json +2 -3
  12. data/chef_dsl_metadata/chef_0.9.10.json +2 -3
  13. data/chef_dsl_metadata/chef_0.9.12.json +2 -3
  14. data/chef_dsl_metadata/chef_0.9.14.json +2 -3
  15. data/chef_dsl_metadata/chef_0.9.16.json +2 -3
  16. data/chef_dsl_metadata/chef_0.9.18.json +2 -3
  17. data/chef_dsl_metadata/chef_0.9.2.json +2 -3
  18. data/chef_dsl_metadata/chef_0.9.4.json +2 -3
  19. data/chef_dsl_metadata/chef_0.9.6.json +2 -3
  20. data/chef_dsl_metadata/chef_0.9.8.json +2 -3
  21. data/chef_dsl_metadata/chef_10.12.0.json +2 -3
  22. data/chef_dsl_metadata/chef_10.14.0.json +2 -3
  23. data/chef_dsl_metadata/chef_10.14.2.json +2 -3
  24. data/chef_dsl_metadata/chef_10.14.4.json +2 -3
  25. data/chef_dsl_metadata/chef_10.16.0.json +2 -3
  26. data/chef_dsl_metadata/chef_10.16.2.json +2 -3
  27. data/chef_dsl_metadata/chef_10.16.4.json +2 -3
  28. data/chef_dsl_metadata/chef_10.16.6.json +2 -3
  29. data/chef_dsl_metadata/chef_10.18.0.json +2 -3
  30. data/chef_dsl_metadata/chef_10.18.2.json +2 -3
  31. data/chef_dsl_metadata/chef_10.20.0.json +2 -3
  32. data/chef_dsl_metadata/chef_10.22.0.json +2 -3
  33. data/chef_dsl_metadata/chef_10.24.0.json +2 -3
  34. data/chef_dsl_metadata/chef_10.24.4.json +2 -3
  35. data/chef_dsl_metadata/chef_10.26.0.json +2 -3
  36. data/chef_dsl_metadata/chef_11.0.0.json +2 -3
  37. data/chef_dsl_metadata/chef_11.2.0.json +2 -3
  38. data/chef_dsl_metadata/chef_11.4.0.json +2 -3
  39. data/chef_dsl_metadata/chef_11.4.2.json +2 -3
  40. data/chef_dsl_metadata/chef_11.4.4.json +2 -3
  41. data/chef_dsl_metadata/chef_11.6.0.json +9734 -0
  42. data/features/007_check_for_undeclared_recipe_dependencies.feature +18 -34
  43. data/features/017_check_for_no_lwrp_notifications.feature +25 -0
  44. data/features/019_check_for_consistent_node_access.feature +1 -0
  45. data/features/033_check_for_missing_template.feature +20 -64
  46. data/features/034_check_for_unused_template_variables.feature +44 -0
  47. data/features/047_check_for_attribute_assignment_without_precedence.feature +47 -0
  48. data/features/048_check_for_shellout.feature +34 -0
  49. data/features/049_check_for_role_name_mismatch_with_file_name.feature +31 -0
  50. data/features/050_check_for_invalid_name.feature +33 -0
  51. data/features/051_check_for_template_partial_loops.feature +21 -0
  52. data/features/command_line_help.feature +15 -0
  53. data/features/ignore_via_line_comments.feature +18 -0
  54. data/features/individual_file.feature +17 -1
  55. data/features/multiple_paths.feature +26 -2
  56. data/features/step_definitions/cookbook_steps.rb +328 -9
  57. data/features/support/command_helpers.rb +71 -10
  58. data/features/support/cookbook_helpers.rb +88 -6
  59. data/lib/foodcritic/api.rb +89 -20
  60. data/lib/foodcritic/command_line.rb +64 -18
  61. data/lib/foodcritic/domain.rb +26 -7
  62. data/lib/foodcritic/dsl.rb +3 -0
  63. data/lib/foodcritic/linter.rb +93 -61
  64. data/lib/foodcritic/rake_task.rb +3 -2
  65. data/lib/foodcritic/rules.rb +105 -14
  66. data/lib/foodcritic/template.rb +34 -1
  67. data/lib/foodcritic/version.rb +1 -1
  68. data/man/foodcritic.1 +13 -1
  69. data/man/foodcritic.1.ronn +9 -0
  70. data/spec/foodcritic/api_spec.rb +210 -1
  71. data/spec/foodcritic/command_line_spec.rb +13 -0
  72. data/spec/foodcritic/domain_spec.rb +40 -5
  73. data/spec/foodcritic/linter_spec.rb +19 -22
  74. data/spec/foodcritic/template_spec.rb +8 -4
  75. data/spec/regression/expected-output.txt +139 -60
  76. metadata +31 -26
@@ -44,11 +44,16 @@ module FoodCritic
44
44
  'FC039' => 'Node method cannot be accessed with key',
45
45
  'FC040' => 'Execute resource used to run git commands',
46
46
  'FC041' => 'Execute resource used to run curl or wget commands',
47
- 'FC042' => 'Prefer include_recipe to require_recipe',
47
+ 'FC042' => 'Prefer include_recipe to require_recipe',
48
48
  'FC043' => 'Prefer new notification syntax',
49
49
  'FC044' => 'Avoid bare attribute keys',
50
50
  'FC045' => 'Consider setting cookbook name in metadata',
51
51
  'FC046' => 'Attribute assignment uses assign unless nil',
52
+ 'FC047' => 'Attribute assignment does not specify precedence',
53
+ 'FC048' => 'Prefer Mixlib::ShellOut',
54
+ 'FC049' => 'Role name does not match containing file name',
55
+ 'FC050' => 'Name includes invalid characters',
56
+ 'FC051' => 'Template partials loop indefinitely',
52
57
  'FCTEST001' => 'Test Rule'
53
58
  }
54
59
 
@@ -97,10 +102,14 @@ module FoodCritic
97
102
  :resource => 'resources/site.rb', :libraries => 'libraries/lib.rb'}[options[:file_type]]
98
103
  end
99
104
  options = {:line => 1, :expect_warning => true, :file => 'recipes/default.rb'}.merge!(options)
105
+ unless options[:file].include?('roles') ||
106
+ options[:file].include?('environments')
107
+ options[:file] = "cookbooks/example/#{options[:file]}"
108
+ end
100
109
  if options[:warning_only]
101
110
  warning = "#{code}: #{WARNINGS[code]}"
102
111
  else
103
- warning = "#{code}: #{WARNINGS[code]}: cookbooks/example/#{options[:file]}:#{options[:line]}#{"\n" if ! options[:line].nil?}"
112
+ warning = "#{code}: #{WARNINGS[code]}: #{options[:file]}:#{options[:line]}#{"\n" if ! options[:line].nil?}"
104
113
  end
105
114
  options[:expect_warning] ? expect_output(warning) : expect_no_output(warning)
106
115
  end
@@ -122,19 +131,29 @@ module FoodCritic
122
131
  expect_output(Regexp.new(expected_switch))
123
132
  end
124
133
 
134
+ def man_page_options
135
+ man_path = Pathname.new(__FILE__) + '../../../man/foodcritic.1.ronn'
136
+ option_lines = File.read(man_path).split('## ').find do |s|
137
+ s.start_with?('OPTIONS')
138
+ end.split("\n").select{|o| o.start_with?(' *')}
139
+ option_lines.map do |o|
140
+ o.sub('`[`no-`]`', '').split('`').select{|f| f.include?('-')}
141
+ end.map do |option|
142
+ {:short => option.first.sub(/^-/, ''),
143
+ :long => option.last.sub(/^--/, '')}
144
+ end.sort_by{|o| o[:short]}
145
+ end
146
+
125
147
  # Assert that the usage message is displayed.
126
148
  #
127
149
  # @param [Boolean] is_exit_zero The exit code to check for.
128
150
  def usage_displayed(is_exit_zero)
129
151
  expect_output 'foodcritic [cookbook_paths]'
130
- expect_usage_option('c', 'chef-version VERSION', 'Only check against rules valid for this version of Chef.')
131
- expect_usage_option('f', 'epic-fail TAGS',
132
- "Fail the build if any of the specified tags are matched ('any' -> fail on any match).")
133
- expect_usage_option('t', 'tags TAGS', 'Only check against rules with the specified tags.')
134
- expect_usage_option('C', '[no-]context', 'Show lines matched against rather than the default summary.')
135
- expect_usage_option('I', 'include PATH', 'Additional rule file path(s) to load.')
136
- expect_usage_option('S', 'search-grammar PATH', 'Specify grammar to use when validating search syntax.')
137
- expect_usage_option('V', 'version', 'Display the foodcritic version.')
152
+
153
+ usage_options.each do |option|
154
+ expect_usage_option(option[:short], option[:long], option[:description])
155
+ end
156
+
138
157
  if is_exit_zero
139
158
  assert_no_error_occurred
140
159
  else
@@ -142,6 +161,48 @@ module FoodCritic
142
161
  end
143
162
  end
144
163
 
164
+ def usage_options
165
+ [
166
+ {:short => 'c', :long => 'chef-version VERSION',
167
+ :description => 'Only check against rules valid for this version of Chef.'},
168
+
169
+ {:short => 'f', :long => 'epic-fail TAGS',
170
+ :description => "Fail the build if any of the specified tags are matched ('any' -> fail on any match)."},
171
+
172
+ {:short => 't', :long => 'tags TAGS',
173
+ :description => 'Only check against rules with the specified tags.'},
174
+
175
+ {:short => 'B', :long => 'cookbook-path PATH',
176
+ :description => 'Cookbook path(s) to check.'},
177
+
178
+ {:short => 'C', :long => '[no-]context',
179
+ :description => 'Show lines matched against rather than the default summary.'},
180
+
181
+ {:short => 'E', :long => 'environment-path PATH',
182
+ :description => 'Environment path(s) to check.'},
183
+
184
+ {:short => 'I', :long => 'include PATH',
185
+ :description => 'Additional rule file path(s) to load.'},
186
+
187
+ {:short => 'R', :long => 'role-path PATH',
188
+ :description => 'Role path(s) to check.'},
189
+
190
+ {:short => 'S', :long => 'search-grammar PATH',
191
+ :description => 'Specify grammar to use when validating search syntax.'},
192
+
193
+ {:short => 'V', :long => 'version',
194
+ :description => 'Display the foodcritic version.'}
195
+
196
+ ]
197
+ end
198
+
199
+ def usage_options_for_diff
200
+ usage_options.map do |o|
201
+ {:short => o[:short],
202
+ :long => o[:long].split(' ').first.sub(/^\[no-\]/, '')}
203
+ end.sort_by{|o| o[:short]}
204
+ end
205
+
145
206
  end
146
207
 
147
208
  # Helpers used when features are executed in-process.
@@ -13,7 +13,7 @@ module FoodCritic
13
13
  # Create a Gemfile for a cookbook
14
14
  def buildable_gemfile
15
15
  write_file 'cookbooks/example/Gemfile', %q{
16
- source :rubygems
16
+ source 'https://rubygems.org/'
17
17
  gem 'rake'
18
18
  gem 'foodcritic', :path => '../../../..'
19
19
  }
@@ -82,8 +82,10 @@ module FoodCritic
82
82
  # @param [Hash] lwrp The options to use for the created LWRP
83
83
  # @option lwrp [Symbol] :default_action One of :no_default_action, :ruby_default_action, :dsl_default_action
84
84
  # @option lwrp [Symbol] :notifies One of :does_not_notify, :does_notify, :does_notify_without_parens, :deprecated_syntax, :class_variable
85
+ # @option lwrp [Symbol] :use_inline_resources Defaults to false
85
86
  def cookbook_with_lwrp(lwrp)
86
- lwrp = {:default_action => false, :notifies => :does_not_notify}.merge!(lwrp)
87
+ lwrp = {:default_action => false, :notifies => :does_not_notify,
88
+ :use_inline_resources => false}.merge!(lwrp)
87
89
  ruby_default_action = %q{
88
90
  def initialize(*args)
89
91
  super
@@ -101,6 +103,7 @@ module FoodCritic
101
103
  :deprecated_syntax => 'new_resource.updated = true',
102
104
  :class_variable => '@updated = true'}
103
105
  write_provider("site", %Q{
106
+ #{'use_inline_resources' if lwrp[:use_inline_resources]}
104
107
  action :create do
105
108
  log "Here is where I would create a site"
106
109
  #{notifications[lwrp[:notifies]]}
@@ -108,6 +111,14 @@ module FoodCritic
108
111
  })
109
112
  end
110
113
 
114
+ def cookbook_with_lwrp_actions(actions)
115
+ write_resource("site", %Q{
116
+ actions #{actions.map{|a| a[:name].inspect}.join(', ')}
117
+ attribute :name, :kind_of => String, :name_attribute => true
118
+ })
119
+ write_provider("site", actions.map{|a| provider_action(a)}.join("\n"))
120
+ end
121
+
111
122
  # Create an cookbook with the maintainer specified in the metadata
112
123
  #
113
124
  # @param [String] name The maintainer name
@@ -136,6 +147,20 @@ module FoodCritic
136
147
  }
137
148
  end
138
149
 
150
+ # Create an environment file
151
+ #
152
+ # @param [Hash] options The options to use for the environment
153
+ # @option options [String] :dir The relative directory to write to
154
+ # @option options [String] :environment_name The name of the environment declared in the file
155
+ # @option options [String] :file_name The containing file relative to the environments directory
156
+ def environment(options={})
157
+ options = {:dir => 'environments'}.merge(options)
158
+ write_file "#{options[:dir]}/#{options[:file_name]}", %Q{
159
+ #{Array(options[:environment_name]).map{|r| "name #{r}"}.join("\n")}
160
+ cookbook "apache2"
161
+ }.strip
162
+ end
163
+
139
164
  # Create a placeholder minitest spec that would be linted due to its path
140
165
  # unless an exclusion is specified.
141
166
  def minitest_spec_attributes
@@ -145,6 +170,30 @@ module FoodCritic
145
170
  }
146
171
  end
147
172
 
173
+ def provider_action(action)
174
+ case action[:notify_type]
175
+ when :none then %Q{
176
+ action #{action[:name].inspect} do
177
+ log "Would take action here"
178
+ end
179
+ }
180
+ when :updated_by_last_action then %Q{
181
+ action #{action[:name].inspect} do
182
+ log "Would take action here"
183
+ # Explicitly update
184
+ new_resource.updated_by_last_action(true)
185
+ end
186
+ }
187
+ when :converge_by then %Q{
188
+ action #{action[:name].inspect} do
189
+ converge_by "#{action[:name]} site" do
190
+ log "Would take action here"
191
+ end
192
+ end
193
+ }
194
+ end
195
+ end
196
+
148
197
  # Create a Rakefile that uses the linter rake task
149
198
  #
150
199
  # @param [Symbol] task Type of task
@@ -273,11 +322,16 @@ module FoodCritic
273
322
  # @param [Hash] dep The options to use for dependency
274
323
  # @option dep [Boolean] :is_declared True if this dependency has been declared in the cookbook metadata
275
324
  # @option dep [Boolean] :is_scoped True if the include_recipe references a specific recipe or the cookbook
325
+ # @option dep [Boolean] :parentheses True if the include_recipe is called with parentheses
276
326
  def recipe_with_dependency(dep)
277
- dep = {:is_scoped => true, :is_declared => true}.merge!(dep)
278
- write_recipe %Q{
279
- include_recipe 'foo#{dep[:is_scoped] ? '::default' : ''}'
280
- }
327
+ dep = {:is_scoped => true, :is_declared => true,
328
+ :parentheses => false}.merge!(dep)
329
+ recipe = "foo#{dep[:is_scoped] ? '::default' : ''}"
330
+ write_recipe(if dep[:parentheses]
331
+ "include_recipe('#{recipe}')"
332
+ else
333
+ "include_recipe '#{recipe}'"
334
+ end)
281
335
  write_metadata %Q{
282
336
  version "1.9.0"
283
337
  depends "#{dep[:is_declared] ? 'foo' : 'dogs'}"
@@ -377,6 +431,34 @@ module FoodCritic
377
431
  }
378
432
  end
379
433
 
434
+ # Create a role file
435
+ #
436
+ # @param [Hash] options The options to use for the role
437
+ # @option options [String] :role_name The name of the role declared in the role file
438
+ # @option options [String] :file_name The containing file relative to the roles directory
439
+ # @option options [Symbol] :format Either :ruby or :json. Default is :ruby
440
+ def role(options={})
441
+ options = {:format => :ruby, :dir => 'roles'}.merge(options)
442
+ content = if options[:format] == :json
443
+ %Q{
444
+ {
445
+ "chef_type": "role",
446
+ "json_class": "Chef::Role",
447
+ #{Array(options[:role_name]).map{|r| "name: #{r},"}.join("\n")}
448
+ "run_list": [
449
+ "recipe[apache2]",
450
+ ]
451
+ }
452
+ }
453
+ else
454
+ %Q{
455
+ #{Array(options[:role_name]).map{|r| "name #{r}"}.join("\n")}
456
+ run_list "recipe[apache2]"
457
+ }
458
+ end
459
+ write_file "#{options[:dir]}/#{options[:file_name]}", content.strip
460
+ end
461
+
380
462
  # Create a rule with the specified Chef version constraints
381
463
  #
382
464
  # @param [String] from_version The from version
@@ -11,6 +11,8 @@ module FoodCritic
11
11
  include FoodCritic::Chef
12
12
  include FoodCritic::Notifications
13
13
 
14
+ class RecursedTooFarError < StandardError; end
15
+
14
16
  # Find attribute access by type.
15
17
  def attribute_access(ast, options = {})
16
18
  options = {:type => :any, :ignore_calls => false}.merge!(options)
@@ -22,10 +24,10 @@ module FoodCritic
22
24
 
23
25
  case options[:type]
24
26
  when :any then
25
- vivified_attribute_access(ast, options[:cookbook_dir]) +
26
- standard_attribute_access(ast, options)
27
+ vivified_attribute_access(ast, options) +
28
+ standard_attribute_access(ast, options)
27
29
  when :vivified then
28
- vivified_attribute_access(ast, options[:cookbook_dir])
30
+ vivified_attribute_access(ast, options)
29
31
  else
30
32
  standard_attribute_access(ast, options)
31
33
  end
@@ -108,7 +110,22 @@ module FoodCritic
108
110
  # depends cbk
109
111
  # end
110
112
  deps = deps.to_a + word_list_values(ast, "//command[ident/@value='depends']")
111
- deps.map{|dep| dep['value']}
113
+ deps.uniq.map{|dep| dep['value'].strip }
114
+ end
115
+
116
+ # The key / value pair in an environment or role ruby file
117
+ def field(ast, field_name)
118
+ if field_name.nil? || field_name.to_s.empty?
119
+ raise ArgumentError, "Field name cannot be nil or empty"
120
+ end
121
+ ast.xpath("//command[ident/@value='#{field_name}']")
122
+ end
123
+
124
+ # The value for a specific key in an environment or role ruby file
125
+ def field_value(ast, field_name)
126
+ field(ast, field_name).xpath('args_add_block/descendant::tstring_content
127
+ [count(ancestor::args_add) = 1][count(ancestor::string_add) = 1]
128
+ /@value').map{|a| a.to_s}.last
112
129
  end
113
130
 
114
131
  # Create a match for a specified file. Use this if the presence of the file
@@ -164,8 +181,13 @@ module FoodCritic
164
181
  filter << '[count(descendant::string_embexpr) = 0]'
165
182
  end
166
183
 
167
- included = ast.xpath(%Q{//command[ident/@value = 'include_recipe']#{filter.join}
168
- [descendant::args_add/string_literal]/descendant::tstring_content})
184
+ string_desc = '[descendant::args_add/string_literal]/descendant::tstring_content'
185
+ included = ast.xpath([
186
+ "//command[ident/@value = 'include_recipe']",
187
+ "//fcall[ident/@value = 'include_recipe']/following-sibling::arg_paren",
188
+ ].map do |recipe_include|
189
+ recipe_include + filter.join + string_desc
190
+ end.join(' | '))
169
191
 
170
192
  # Hash keyed by recipe name with matched nodes.
171
193
  included.inject(Hash.new([])){|h, i| h[i['value']] += [i]; h}
@@ -191,8 +213,7 @@ module FoodCritic
191
213
  # Read the AST for the given Ruby source file
192
214
  def read_ast(file)
193
215
  source = if file.to_s.split(File::SEPARATOR).include?('templates')
194
- Template::ExpressionExtractor.new.extract(
195
- File.read(file)).map{|e| e[:code]}.join(';')
216
+ template_expressions_only(file)
196
217
  else
197
218
  File.read(file)
198
219
  end
@@ -312,10 +333,23 @@ module FoodCritic
312
333
  end
313
334
  end
314
335
 
336
+ def templates_included(all_templates, template_path, depth=1)
337
+ raise RecursedTooFarError.new(template_path) if depth > 10
338
+ partials = read_ast(template_path).xpath('//*[self::command or
339
+ child::fcall][descendant::ident/@value="render"]//args_add/
340
+ string_literal//tstring_content/@value').map{|p| p.to_s}
341
+ Array(template_path) + partials.map do |included_partial|
342
+ partial_path = Array(all_templates).find do |path|
343
+ File.basename(path) == included_partial.to_s
344
+ end
345
+ Array(partial_path) + templates_included(all_templates, partial_path, depth + 1)
346
+ end.flatten.uniq
347
+ end
348
+
315
349
  # Templates in the current cookbook
316
350
  def template_paths(recipe_path)
317
- Dir[Pathname.new(recipe_path).dirname.dirname + 'templates' +
318
- '**/*'].select{|path| File.file?(path)}
351
+ Dir.glob(Pathname.new(recipe_path).dirname.dirname + 'templates' +
352
+ '**/*', File::FNM_DOTMATCH).select{|path| File.file?(path)}
319
353
  end
320
354
 
321
355
  private
@@ -390,6 +424,12 @@ module FoodCritic
390
424
  end
391
425
  end
392
426
 
427
+ def ignore_attributes_xpath(ignores)
428
+ Array(ignores).map do |ignore|
429
+ "[count(descendant::*[@value='#{ignore}']) = 0]"
430
+ end.join
431
+ end
432
+
393
433
  def node_method?(meth, cookbook_dir)
394
434
  chef_dsl_methods.include?(meth) || patched_node_method?(meth, cookbook_dir)
395
435
  end
@@ -433,7 +473,22 @@ module FoodCritic
433
473
  class AttFilter
434
474
  def is_att_type(value)
435
475
  return [] unless value.respond_to?(:select)
436
- value.select{|n| %w{node default override set normal}.include?(n.to_s)}
476
+ value.select do |n|
477
+ %w{
478
+ automatic_attrs
479
+ default
480
+ default_unless
481
+ force_default
482
+ force_override
483
+ node
484
+ normal
485
+ normal_unless
486
+ override
487
+ override_unless
488
+ set
489
+ set_unless
490
+ }.include?(n.to_s)
491
+ end
437
492
  end
438
493
  end
439
494
 
@@ -444,31 +499,45 @@ module FoodCritic
444
499
  end.inject(:+)
445
500
  else
446
501
  type = if options[:type] == :string
447
- 'tstring_content'
448
- else
449
- '*[self::symbol or self::dyna_symbol]'
450
- end
502
+ 'tstring_content'
503
+ else
504
+ '*[self::symbol or self::dyna_symbol]'
505
+ end
451
506
  expr = '//*[self::aref_field or self::aref][count(method_add_arg) = 0]'
452
507
  expr += '[count(is_att_type(descendant::var_ref/ident/@value)) =
453
508
  count(descendant::var_ref/ident/@value)]'
454
509
  expr += '[is_att_type(descendant::ident'
455
510
  expr += '[not(ancestor::aref/call)]' if options[:ignore_calls]
456
- expr += "/@value)]/descendant::#{type}"
457
- if options[:type] == :string
511
+ expr += '/@value)]'
512
+ expr += ignore_attributes_xpath(options[:ignore])
513
+ expr += "/descendant::#{type}"
514
+ if options[:type] == :string
458
515
  expr += '[count(ancestor::dyna_symbol) = 0]'
459
516
  end
460
517
  ast.xpath(expr, AttFilter.new).sort
461
518
  end
462
519
  end
463
520
 
464
- def vivified_attribute_access(ast, cookbook_dir)
465
- calls = ast.xpath(%q{//*[self::call or self::field]
521
+ def template_expressions_only(file)
522
+ exprs = Template::ExpressionExtractor.new.extract(File.read(file))
523
+ lines = Array.new(exprs.map{|e| e[:line]}.max || 0, '')
524
+ exprs.each do |e|
525
+ lines[e[:line] -1] += ';' unless lines[e[:line] -1].empty?
526
+ lines[e[:line] -1] += e[:code]
527
+ end
528
+ lines.join("\n")
529
+ end
530
+
531
+ def vivified_attribute_access(ast, options={})
532
+ calls = ast.xpath(%Q{//*[self::call or self::field]
466
533
  [is_att_type(vcall/ident/@value) or is_att_type(var_ref/ident/@value)]
534
+ #{ignore_attributes_xpath(options[:ignore])}
467
535
  [@value='.'][count(following-sibling::arg_paren) = 0]}, AttFilter.new)
468
536
  calls.select do |call|
469
537
  call.xpath("aref/args_add_block").size == 0 and
470
538
  (call.xpath("descendant::ident").size > 1 and
471
- ! node_method?(call.xpath("ident/@value").to_s.to_sym, cookbook_dir))
539
+ ! node_method?(call.xpath("ident/@value").to_s.to_sym,
540
+ options[:cookbook_dir]))
472
541
  end.sort
473
542
  end
474
543