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
@@ -9,7 +9,7 @@ module FoodCritic
9
9
 
10
10
  def initialize(name = :foodcritic)
11
11
  @name = name
12
- @files = Dir.pwd
12
+ @files = [Dir.pwd]
13
13
  @options = {}
14
14
  yield self if block_given?
15
15
  define
@@ -17,6 +17,7 @@ module FoodCritic
17
17
 
18
18
  def options
19
19
  {:fail_tags => ['correctness'], # differs to default cmd-line behaviour
20
+ :cookbook_paths => @files,
20
21
  :exclude_paths => ['test/**/*', 'spec/**/*', 'features/**/*']
21
22
  }.merge(@options)
22
23
  end
@@ -24,7 +25,7 @@ module FoodCritic
24
25
  def define
25
26
  desc "Lint Chef cookbooks"
26
27
  task(name) do
27
- result = FoodCritic::Linter.new.check(files, options)
28
+ result = FoodCritic::Linter.new.check(options)
28
29
  if result.warnings.any?
29
30
  puts result
30
31
  end
@@ -208,12 +208,30 @@ rule "FC017", "LWRP does not notify when updated" do
208
208
  version >= gem_version("0.7.12")
209
209
  end
210
210
  provider do |ast, filename|
211
- if ast.xpath(%q{//*[self::call or self::command_call]/
212
- *[self::vcall or self::var_ref/ident/
213
- @value='new_resource']/../
214
- ident[@value='updated_by_last_action']}).empty?
215
- [file_match(filename)]
211
+
212
+ use_inline_resources = ! ast.xpath('//*[self::vcall or self::var_ref]/ident
213
+ [@value="use_inline_resources"]').empty?
214
+
215
+ unless use_inline_resources
216
+ actions = ast.xpath('//method_add_block/command[ident/@value="action"]/
217
+ args_add_block/descendant::symbol/ident')
218
+
219
+ actions.reject do |action|
220
+ blk = action.xpath('ancestor::command[1]/
221
+ following-sibling::*[self::do_block or self::brace_block]')
222
+ empty = ! blk.xpath('stmts_add/void_stmt').empty?
223
+ converge_by = ! blk.xpath('descendant::*[self::command or self::fcall]
224
+ /ident[@value="converge_by"]').empty?
225
+
226
+ updated_by_last_action = ! blk.xpath('descendant::*[self::call or
227
+ self::command_call]/*[self::vcall or self::var_ref/ident/
228
+ @value="new_resource"]/../ident[@value="updated_by_last_action"]
229
+ ').empty?
230
+
231
+ empty || converge_by || updated_by_last_action
232
+ end
216
233
  end
234
+
217
235
  end
218
236
  end
219
237
 
@@ -240,7 +258,7 @@ rule "FC019", "Access node attributes in a consistent manner" do
240
258
  types = [:string, :symbol, :vivified].map do |type|
241
259
  {:access_type => type, :count => files.map do |file|
242
260
  attribute_access(file[:ast], :type => type, :ignore_calls => true,
243
- :cookbook_dir => cookbook_dir).tap do |ast|
261
+ :cookbook_dir => cookbook_dir, :ignore => 'run_state').tap do |ast|
244
262
  unless ast.empty?
245
263
  (asts[type] ||= []) << {:ast => ast, :path => file[:path]}
246
264
  end
@@ -466,17 +484,29 @@ end
466
484
  rule "FC034", "Unused template variables" do
467
485
  tags %w{correctness}
468
486
  recipe do |ast,filename|
469
- Array(resource_attributes_by_type(ast)['template']).select do
470
- |t| t['variables'] and t['variables'].respond_to?(:xpath)
487
+ Array(resource_attributes_by_type(ast)['template']).select do |t|
488
+ t['variables'] and t['variables'].respond_to?(:xpath)
471
489
  end.map do |resource|
472
- template_path = template_paths(filename).find do |p|
473
- File.basename(p) == resource['source']
490
+ all_templates = template_paths(filename)
491
+ template_path = all_templates.find do |path|
492
+ File.basename(path) == template_file(resource)
474
493
  end
475
494
  next unless template_path
476
- passed_vars = resource['variables'].xpath('symbol/ident/@value').map{|tv| tv.to_s}
477
- template_vars = read_ast(template_path).xpath('//var_ref/ivar/' +
478
- '@value').map{|v| v.to_s.sub(/^@/, '')}
479
- file_match(template_path) unless (passed_vars - template_vars).empty?
495
+ passed_vars = resource['variables'].xpath(
496
+ 'symbol/ident/@value').map{|tv| tv.to_s}
497
+
498
+ begin
499
+ template_vars = templates_included(
500
+ all_templates, template_path).map do |template|
501
+ read_ast(template).xpath('//var_ref/ivar/@value').map do |v|
502
+ v.to_s.sub(/^@/, '')
503
+ end
504
+ end.flatten
505
+
506
+ file_match(template_path) unless (passed_vars - template_vars).empty?
507
+ rescue RecursedTooFarError
508
+ []
509
+ end
480
510
  end.compact
481
511
  end
482
512
  end
@@ -608,3 +638,64 @@ rule "FC046", "Attribute assignment uses assign unless nil" do
608
638
  attribute_access(ast).map{|a| a.xpath('ancestor::opassign/op[@value="||="]')}
609
639
  end
610
640
  end
641
+
642
+ rule "FC047", "Attribute assignment does not specify precedence" do
643
+ tags %w{attributes correctness}
644
+ recipe do |ast|
645
+ attribute_access(ast).map do |att|
646
+ exclude_att_types = '[count(following-sibling::ident[
647
+ is_att_type(@value) or @value = "run_state"]) = 0]'
648
+ att.xpath(%Q{ancestor::assign[*[self::field | self::aref_field]
649
+ [descendant::*[self::vcall | self::var_ref][ident/@value="node"]
650
+ #{exclude_att_types}]]}, AttFilter.new) +
651
+ att.xpath(%Q{ancestor::binary[@value="<<"]/*[position() = 1][self::aref]
652
+ [descendant::*[self::vcall | self::var_ref]#{exclude_att_types}
653
+ /ident/@value="node"]}, AttFilter.new)
654
+ end
655
+ end
656
+ end
657
+
658
+ rule "FC048", "Prefer Mixlib::ShellOut" do
659
+ tags %w{style processes}
660
+ recipe do |ast|
661
+ ast.xpath('//xstring_literal | //*[self::command or self::fcall]/
662
+ ident[@value="system"][count(following-sibling::args_add_block/
663
+ descendant::kw[@value="true" or @value="false"]) = 0]')
664
+ end
665
+ end
666
+
667
+ rule "FC049", "Role name does not match containing file name" do
668
+ tags %w{style roles}
669
+ role do |ast, filename|
670
+ role_name_specified = field_value(ast, :name)
671
+ role_name_file = Pathname.new(filename).basename.sub_ext('').to_s
672
+ if role_name_specified and role_name_specified != role_name_file
673
+ field(ast, :name)
674
+ end
675
+ end
676
+ end
677
+
678
+ rule "FC050", "Name includes invalid characters" do
679
+ tags %w{correctness environments roles}
680
+ def invalid_name(ast)
681
+ field(ast, :name) unless field_value(ast, :name) =~ /^[a-zA-Z0-9_\-]+$/
682
+ end
683
+ environment{|ast| invalid_name(ast)}
684
+ role{|ast| invalid_name(ast)}
685
+ end
686
+
687
+ rule "FC051", "Template partials loop indefinitely" do
688
+ tags %w{correctness}
689
+ recipe do |_,filename|
690
+ cbk_templates = template_paths(filename)
691
+
692
+ cbk_templates.select do |template|
693
+ begin
694
+ templates_included(cbk_templates, template)
695
+ false
696
+ rescue RecursedTooFarError
697
+ true
698
+ end
699
+ end.map{|t| file_match(t)}
700
+ end
701
+ end
@@ -13,7 +13,7 @@ module FoodCritic
13
13
  def extract(template_code)
14
14
  @expressions = []
15
15
  convert(template_code)
16
- @expressions
16
+ expressions(template_code)
17
17
  end
18
18
 
19
19
  def add_expr(src, code, indicator)
@@ -38,6 +38,39 @@ module FoodCritic
38
38
  @expressions << {:type => :statement, :code => code.strip}
39
39
  end
40
40
 
41
+ private
42
+
43
+ def expressions(template_code)
44
+ expr_lines = expressions_with_lines(template_code)
45
+ expr_lines.map do |expr, line|
46
+ e = @expressions.find{|e| e[:code] == expr}
47
+ {:code => expr, :type => e[:type], :line => line} if e
48
+ end.compact
49
+ end
50
+
51
+ def expressions_with_lines(template_code)
52
+ lines = lines_with_offsets(template_code)
53
+ expression_offsets(template_code).map do |expr_offset, code|
54
+ [code, lines.find {|line, offset| offset >= expr_offset}.first]
55
+ end
56
+ end
57
+
58
+ def expression_offsets(template_code)
59
+ expr_offsets = []
60
+ template_code.scan(DEFAULT_REGEXP) do |m|
61
+ expr_offsets << [Regexp.last_match.offset(0).first, m[1].strip]
62
+ end
63
+ expr_offsets
64
+ end
65
+
66
+ def lines_with_offsets(template_code)
67
+ line_offsets = []
68
+ template_code.scan(/$/) do |m|
69
+ line_offsets << Regexp.last_match.offset(0).first
70
+ end
71
+ line_offsets.each_with_index.map{| pos, ln| [ln +1, pos]}
72
+ end
73
+
41
74
  end
42
75
 
43
76
  end
@@ -1,4 +1,4 @@
1
1
  module FoodCritic
2
2
  # The current version of foodcritic
3
- VERSION = '2.2.0'
3
+ VERSION = '3.0.0'
4
4
  end
data/man/foodcritic.1 CHANGED
@@ -1,7 +1,7 @@
1
1
  .\" generated with Ronn/v0.7.3
2
2
  .\" http://github.com/rtomayko/ronn/tree/0.7.3
3
3
  .
4
- .TH "FOODCRITIC" "1" "July 2013" "" ""
4
+ .TH "FOODCRITIC" "1" "August 2013" "" ""
5
5
  .
6
6
  .SH "NAME"
7
7
  \fBfoodcritic\fR \- lint tool for chef cookbooks
@@ -30,14 +30,26 @@ Exit non\-zero if any of the specified tags are matched\. Use the pseudo\-tag \f
30
30
  Only check against rules valid for this version of Chef\.
31
31
  .
32
32
  .TP
33
+ \fB\-B\fR, \fB\-\-cookbook\-path\fR
34
+ Cookbook path(s) to check\.
35
+ .
36
+ .TP
33
37
  \fB\-C\fR, \fB\-\-\fR[\fBno\-\fR]\fBcontext\fR
34
38
  Show lines matched against rather than the default summary\.
35
39
  .
36
40
  .TP
41
+ \fB\-E\fR, \fB\-\-environment\-path\fR
42
+ Environment path(s) to check\.
43
+ .
44
+ .TP
37
45
  \fB\-I\fR, \fB\-\-include\fR \fIPATH\fR
38
46
  Additional rule file path(s) to load\.
39
47
  .
40
48
  .TP
49
+ \fB\-R\fR, \fB\-\-role\-path\fR
50
+ Role path(s) to check\.
51
+ .
52
+ .TP
41
53
  \fB\-S\fR, \fB\-\-search\-grammar\fR \fIPATH\fR
42
54
  Specify grammar to use when validating search syntax\. (Default: the grammar of any installed Chef)
43
55
  .
@@ -28,12 +28,21 @@ poor style.
28
28
  * `-c`, `--chef-version` <VERSION>:
29
29
  Only check against rules valid for this version of Chef.
30
30
 
31
+ * `-B`, `--cookbook-path`:
32
+ Cookbook path(s) to check.
33
+
31
34
  * `-C`, `--`[`no-`]`context`:
32
35
  Show lines matched against rather than the default summary.
33
36
 
37
+ * `-E`, `--environment-path`:
38
+ Environment path(s) to check.
39
+
34
40
  * `-I`, `--include` <PATH>:
35
41
  Additional rule file path(s) to load.
36
42
 
43
+ * `-R`, `--role-path`:
44
+ Role path(s) to check.
45
+
37
46
  * `-S`, `--search-grammar` <PATH>:
38
47
  Specify grammar to use when validating search syntax.
39
48
  (Default: the grammar of any installed Chef)
@@ -21,6 +21,8 @@ describe FoodCritic::Api do
21
21
  :chef_solo_search_supported?,
22
22
  :cookbook_name,
23
23
  :declared_dependencies,
24
+ :field,
25
+ :field_value,
24
26
  :file_match,
25
27
  :find_resources,
26
28
  :gem_version,
@@ -40,9 +42,10 @@ describe FoodCritic::Api do
40
42
  :ruby_code?,
41
43
  :searches,
42
44
  :standard_cookbook_subdirs,
43
- :supported_platforms,
45
+ :supported_platforms,
44
46
  :template_file,
45
47
  :template_paths,
48
+ :templates_included,
46
49
  :valid_query?
47
50
  ])
48
51
  end
@@ -87,6 +90,51 @@ describe FoodCritic::Api do
87
90
  ast = parse_ast(%q{baz = search(:node, "name:#{node['foo']['bar']}")[0]})
88
91
  api.attribute_access(ast, :type => :symbol).must_be_empty
89
92
  end
93
+ describe :ignoring_attributes do
94
+ it "doesn't ignore run_state by default for backwards compatibility" do
95
+ ast = parse_ast("node.run_state['bar'] = 'baz'")
96
+ api.attribute_access(ast).wont_be_empty
97
+ end
98
+ it "allows run_state to be ignored" do
99
+ ast = parse_ast("node.run_state['bar'] = 'baz'")
100
+ api.attribute_access(ast, :ignore => ['run_state']).must_be_empty
101
+ end
102
+ it "allows run_state to be ignored (symbols access)" do
103
+ ast = parse_ast("node.run_state[:bar] = 'baz'")
104
+ api.attribute_access(ast, :ignore => ['run_state']).must_be_empty
105
+ end
106
+ it "allows any attribute to be ignored" do
107
+ ast = parse_ast("node['bar'] = 'baz'")
108
+ api.attribute_access(ast, :ignore => ['bar']).must_be_empty
109
+ end
110
+ it "allows any attribute to be ignored (symbols access)" do
111
+ ast = parse_ast("node[:bar] = 'baz'")
112
+ api.attribute_access(ast, :ignore => ['bar']).must_be_empty
113
+ end
114
+ it "allows any attribute to be ignored (dot access)" do
115
+ ast = parse_ast("node.bar = 'baz'")
116
+ api.attribute_access(ast, :ignore => ['bar']).must_be_empty
117
+ end
118
+ it "includes the children of attributes" do
119
+ ast = parse_ast("node['foo']['bar'] = 'baz'")
120
+ api.attribute_access(ast).map{|a| a['value']}.must_equal(%w{foo bar})
121
+ end
122
+ it "does not include children of removed attributes" do
123
+ ast = parse_ast("node['foo']['bar'] = 'baz'")
124
+ api.attribute_access(ast, :ignore => ['foo']).must_be_empty
125
+ end
126
+ it "coerces ignore values to enumerate them" do
127
+ ast = parse_ast("node.run_state['bar'] = 'baz'")
128
+ api.attribute_access(ast, :ignore => 'run_state').must_be_empty
129
+ end
130
+ it "can ignore multiple attributes" do
131
+ ast = parse_ast(%q{
132
+ node['bar'] = 'baz'
133
+ node.foo = 'baz'
134
+ })
135
+ api.attribute_access(ast, :ignore => %w{foo bar}).must_be_empty
136
+ end
137
+ end
90
138
  end
91
139
 
92
140
  describe "#checks_for_chef_solo?" do
@@ -184,6 +232,86 @@ describe FoodCritic::Api do
184
232
  end
185
233
  end
186
234
 
235
+ describe "#field" do
236
+ describe :simple_ast do
237
+ let(:ast){ parse_ast('name "webserver"') }
238
+ it "raises if the field name is nil" do
239
+ lambda{api.field(ast, nil)}.must_raise ArgumentError
240
+ end
241
+ it "raises if the field name is empty" do
242
+ lambda{api.field(ast, '')}.must_raise ArgumentError
243
+ end
244
+ it "returns empty if the field is not present" do
245
+ api.field(ast, :common_name).must_be_empty
246
+ end
247
+ it "accepts a string for the field name" do
248
+ api.field(ast, 'name').wont_be_empty
249
+ end
250
+ it "accepts a symbol for the field name" do
251
+ api.field(ast, :name).wont_be_empty
252
+ end
253
+ end
254
+ it "returns fields when the value is an embedded string expression" do
255
+ ast = parse_ast(%q{
256
+ name "#{foo}_#{bar}"
257
+ }.strip)
258
+ api.field(ast, :name).size.must_equal 1
259
+ end
260
+ it "returns fields when the value is a method call" do
261
+ ast = parse_ast(%q{
262
+ name generate_name
263
+ }.strip)
264
+ api.field(ast, :name).size.must_equal 1
265
+ end
266
+ it "returns both fields if the field is specified twice" do
267
+ ast = parse_ast(%q{
268
+ name "webserver"
269
+ name "database"
270
+ }.strip)
271
+ api.field(ast, :name).size.must_equal 2
272
+ end
273
+ end
274
+
275
+ describe "#field_value" do
276
+ describe :simple_ast do
277
+ let(:ast){ parse_ast('name "webserver"') }
278
+ it "raises if the field name is nil" do
279
+ lambda{api.field_value(ast, nil)}.must_raise ArgumentError
280
+ end
281
+ it "raises if the field name is empty" do
282
+ lambda{api.field_value(ast, '')}.must_raise ArgumentError
283
+ end
284
+ it "is falsy if the field is not present" do
285
+ refute api.field_value(ast, :common_name)
286
+ end
287
+ it "accepts a string for the field name" do
288
+ api.field_value(ast, 'name').must_equal 'webserver'
289
+ end
290
+ it "accepts a symbol for the field name" do
291
+ api.field_value(ast, :name).must_equal 'webserver'
292
+ end
293
+ end
294
+ it "is falsy when the value is an embedded string expression" do
295
+ ast = parse_ast(%q{
296
+ name "#{foo}_#{bar}"
297
+ }.strip)
298
+ refute api.field_value(ast, :name)
299
+ end
300
+ it "is falsy when the value is a method call" do
301
+ ast = parse_ast(%q{
302
+ name generate_name('foo')
303
+ }.strip)
304
+ refute api.field_value(ast, :name)
305
+ end
306
+ it "returns the last value if the field is specified twice" do
307
+ ast = parse_ast(%q{
308
+ name "webserver"
309
+ name "database"
310
+ }.strip)
311
+ api.field_value(ast, :name).must_equal 'database'
312
+ end
313
+ end
314
+
187
315
  describe "#file_match" do
188
316
  it "includes the provided filename in the match" do
189
317
  api.file_match("foo.rb")[:filename].must_equal "foo.rb"
@@ -1612,4 +1740,85 @@ describe FoodCritic::Api do
1612
1740
  end
1613
1741
  end
1614
1742
 
1743
+ describe "#templates_included" do
1744
+
1745
+ def all_templates
1746
+ [
1747
+ 'templates/default/main.erb',
1748
+ 'templates/default/included_1.erb',
1749
+ 'templates/default/included_2.erb'
1750
+ ]
1751
+ end
1752
+
1753
+ def template_ast(content)
1754
+ parse_ast(FoodCritic::Template::ExpressionExtractor.new.extract(
1755
+ content).map{|e| e[:code]}.join(';'))
1756
+ end
1757
+
1758
+ it "returns the path of the containing template when there are no partials" do
1759
+ ast = parse_ast('<%= foo.erb %>')
1760
+ api.stub :read_ast, ast do
1761
+ api.templates_included(['foo.erb'], 'foo.erb').must_equal ['foo.erb']
1762
+ end
1763
+ end
1764
+
1765
+ it "returns the path of the containing template and any partials" do
1766
+ api.instance_variable_set(:@asts, {
1767
+ :main => template_ast('<%= render "included_1.erb" %>
1768
+ <%= render "included_2.erb" %>'),
1769
+ :ok => template_ast('<%= @foo %>')
1770
+ })
1771
+ def api.read_ast(path)
1772
+ case path
1773
+ when /main/ then @asts[:main]
1774
+ else @asts[:ok]
1775
+ end
1776
+ end
1777
+ api.templates_included(all_templates,
1778
+ 'templates/default/main.erb').must_equal(
1779
+ ['templates/default/main.erb',
1780
+ 'templates/default/included_1.erb',
1781
+ 'templates/default/included_2.erb']
1782
+ )
1783
+ end
1784
+
1785
+ it "doesn't mistake render options for partial template names" do
1786
+ api.instance_variable_set(:@asts, {
1787
+ :main => template_ast('<%= render "included_1.erb",
1788
+ :variables => {:foo => "included_2.erb"} %>'),
1789
+ :ok => template_ast('<%= @foo %>')
1790
+ })
1791
+ def api.read_ast(path)
1792
+ case path
1793
+ when /main/ then @asts[:main]
1794
+ else @asts[:ok]
1795
+ end
1796
+ end
1797
+ api.templates_included(all_templates,
1798
+ 'templates/default/main.erb').must_equal(
1799
+ ['templates/default/main.erb', 'templates/default/included_1.erb']
1800
+ )
1801
+ end
1802
+
1803
+ it "raises if included partials have cycles" do
1804
+ api.instance_variable_set(:@asts, {
1805
+ :main => template_ast('<%= render "included_1.erb" %>
1806
+ <%= render "included_2.erb" %>'),
1807
+ :loop => template_ast('<%= render "main.erb" %>'),
1808
+ :ok => template_ast('<%= foo %>')
1809
+ })
1810
+ def api.read_ast(path)
1811
+ case path
1812
+ when /main/ then @asts[:main]
1813
+ when /included_2/ then @asts[:loop]
1814
+ else @asts[:ok]
1815
+ end
1816
+ end
1817
+ err = lambda do
1818
+ api.templates_included(all_templates, 'templates/default/main.erb')
1819
+ end.must_raise(FoodCritic::Api::RecursedTooFarError)
1820
+ err.message.must_equal 'templates/default/main.erb'
1821
+ end
1822
+ end
1823
+
1615
1824
  end