Module: FoodCritic::Api
- Includes:
- AST, Chef, Notifications, XML
- Defined in:
- lib/foodcritic/api.rb
Overview
Helper methods that form part of the Rules DSL.
Defined Under Namespace
Classes: AttFilter, RecursedTooFarError
Instance Method Summary collapse
-
#ast_cache(size = nil) ⇒ Object
Returns a global LRU cache holding the AST of a file.
-
#attribute_access(ast, options = {}) ⇒ Object
Find attribute access by type.
-
#cookbook_base_path(file) ⇒ String
The absolute path of a cookbook from the specified file.
-
#cookbook_maintainer(file) ⇒ String
Return metadata maintainer property given any file in the cookbook.
-
#cookbook_maintainer_email(file) ⇒ String
Return metadata maintainer_email property given any file in the cookbook.
-
#cookbook_name(file) ⇒ String
The name of the cookbook containing the specified file.
-
#declared_dependencies(ast) ⇒ Object
The dependencies declared in cookbook metadata.
-
#ensure_file_exists(basepath, filepath) ⇒ String
Match unless a file exists using the basepath of the cookbook and the filepath.
-
#field(ast, field_name) ⇒ Nokogiri::XML::NodeSet
Look for a method call with a given name.
-
#field_value(ast, field_name) ⇒ Object
The value for a specific key in an environment or role ruby file.
-
#file_match(file) ⇒ Object
Create a match for a specified file.
-
#find_resources(ast, options = {}) ⇒ Object
Find Chef resources of the specified type.
-
#gem_version(version) ⇒ Object
Helper to return a comparable version for a string.
-
#included_recipes(ast, options = { with_partial_names: true }) ⇒ Object
Retrieve the recipes that are included within the given recipe AST.
-
#json_file_to_hash(filename) ⇒ Hash
Give a filename path it returns the hash of the JSON contents.
-
#literal_searches(ast) ⇒ Object
Searches performed by the specified recipe that are literal strings.
-
#match(node) ⇒ Object
Create a match from the specified node.
-
#metadata_field(file, field) ⇒ String
Retrieves a value of a metadata field.
-
#read_ast(file) ⇒ Object
Read the AST for the given Ruby source file.
-
#resource_attribute(resource, name) ⇒ Object
Retrieve a single-valued attribute from the specified resource.
-
#resource_attributes(resource, options = {}) ⇒ Object
Retrieve all attributes from the specified resource.
-
#resource_attributes_by_type(ast) ⇒ Object
Resources keyed by type, with an array of matching nodes for each.
-
#resource_name(resource, options = {}) ⇒ Object
Retrieve the name attribute associated with the specified resource.
-
#resource_type(resource) ⇒ Object
Return the type, e.g.
-
#resources_by_type(ast) ⇒ Object
Resources in an AST, keyed by type.
-
#ruby_code?(str) ⇒ Boolean
Does the provided string look like ruby code?.
-
#searches(ast) ⇒ Object
Searches performed by the provided AST.
-
#standard_cookbook_subdirs ⇒ array
The list of standard cookbook sub-directories.
-
#supported_platforms(ast) ⇒ Array<Hash>
Platforms declared as supported in cookbook metadata.
-
#template_file(resource) ⇒ Object
Template filename.
-
#template_paths(recipe_path) ⇒ Object
Templates in the current cookbook.
- #templates_included(all_templates, template_path, depth = 1) ⇒ Object
Methods included from Notifications
Methods included from Chef
#chef_dsl_methods, #chef_node_methods, #resource_action?, #resource_attribute?, #valid_query?
Instance Method Details
#ast_cache(size = nil) ⇒ Object
Returns a global LRU cache holding the AST of a file
260 261 262 263 264 265 266 |
# File 'lib/foodcritic/api.rb', line 260 def ast_cache(size = nil) @@ast_cache ||= Rufus::Lru::Hash.new(size || 5) if size && @@ast_cache.maxsize != size @@ast_cache.maxsize = size end @@ast_cache end |
#attribute_access(ast, options = {}) ⇒ Object
Find attribute access by type.
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
# File 'lib/foodcritic/api.rb', line 28 def attribute_access(ast, = {}) = { type: :any, ignore_calls: false }.merge!() return [] unless ast.respond_to?(:xpath) unless [:any, :string, :symbol, :vivified].include?([:type]) raise ArgumentError, "Node type not recognised" end case [:type] when :any then vivified_attribute_access(ast, ) + standard_attribute_access(ast, ) when :vivified then vivified_attribute_access(ast, ) else standard_attribute_access(ast, ) end end |
#cookbook_base_path(file) ⇒ String
The absolute path of a cookbook from the specified file.
53 54 55 56 57 58 59 60 61 62 63 64 |
# File 'lib/foodcritic/api.rb', line 53 def cookbook_base_path(file) file = File.(file) # make sure we get an absolute path file = File.dirname(file) unless File.directory?(file) # get the dir only # get list of items in the dir and intersect with metadata array. # until we get an interfact (we have a metadata) walk up the dir structure until (Dir.entries(file) & %w{metadata.rb metadata.json}).any? file = File.dirname(file) end file end |
#cookbook_maintainer(file) ⇒ String
Return metadata maintainer property given any file in the cookbook
113 114 115 116 117 |
# File 'lib/foodcritic/api.rb', line 113 def cookbook_maintainer(file) raise ArgumentError, "File cannot be nil or empty" if file.to_s.empty? (file, "maintainer") end |
#cookbook_maintainer_email(file) ⇒ String
Return metadata maintainer_email property given any file in the cookbook
123 124 125 126 127 |
# File 'lib/foodcritic/api.rb', line 123 def cookbook_maintainer_email(file) raise ArgumentError, "File cannot be nil or empty" if file.to_s.empty? (file, "maintainer_email") end |
#cookbook_name(file) ⇒ String
The name of the cookbook containing the specified file.
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/foodcritic/api.rb', line 93 def cookbook_name(file) raise ArgumentError, "File cannot be nil or empty" if file.to_s.empty? # Name is a special case as we want to fallback to the cookbook directory # name if metadata_field fails begin (file, "name") rescue RuntimeError until (file.split(File::SEPARATOR) & standard_cookbook_subdirs).empty? file = File.absolute_path(File.dirname(file.to_s)) end file = File.dirname(file) unless File.extname(file).empty? File.basename(file) end end |
#declared_dependencies(ast) ⇒ Object
The dependencies declared in cookbook metadata.
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
# File 'lib/foodcritic/api.rb', line 130 def declared_dependencies(ast) raise_unless_xpath!(ast) deps = [] # String literals. # # depends 'foo' deps += field(ast, "depends").xpath("descendant::args_add/descendant::tstring_content[1]") # Quoted word arrays are also common. # # %w{foo bar baz}.each do |cbk| # depends cbk # end deps += word_list_values(field(ast, "depends")) deps.uniq! deps.map! { |dep| dep["value"].strip } deps end |
#ensure_file_exists(basepath, filepath) ⇒ String
Match unless a file exists using the basepath of the cookbook and the filepath
22 23 24 25 |
# File 'lib/foodcritic/api.rb', line 22 def ensure_file_exists(basepath, filepath) path = File.join(basepath, filepath) [file_match(path)] unless File.exist?(path) end |
#field(ast, field_name) ⇒ Nokogiri::XML::NodeSet
Look for a method call with a given name.
155 156 157 158 159 160 |
# File 'lib/foodcritic/api.rb', line 155 def field(ast, field_name) if field_name.nil? || field_name.to_s.empty? raise ArgumentError, "Field name cannot be nil or empty" end ast.xpath("(.//command[ident/@value='#{field_name}']|.//fcall[ident/@value='#{field_name}']/..)") end |
#field_value(ast, field_name) ⇒ Object
The value for a specific key in an environment or role ruby file
163 164 165 166 167 |
# File 'lib/foodcritic/api.rb', line 163 def field_value(ast, field_name) field(ast, field_name).xpath('.//args_add_block//tstring_content [count(ancestor::args_add) = 1][count(ancestor::string_add) = 1] /@value').map { |a| a.to_s }.last end |
#file_match(file) ⇒ Object
Create a match for a specified file. Use this if the presence of the file triggers the warning rather than content.
171 172 173 174 |
# File 'lib/foodcritic/api.rb', line 171 def file_match(file) raise ArgumentError, "Filename cannot be nil" if file.nil? { filename: file, matched: file, line: 1, column: 1 } end |
#find_resources(ast, options = {}) ⇒ Object
Find Chef resources of the specified type. TODO: Include blockless resources
These are equivalent:
find_resources(ast)
find_resources(ast, type: :any)
Restrict to a specific type of resource(s):
find_resources(ast, type: 'service')
find_resources(ast, type: %w(service file))
189 190 191 192 193 194 195 196 197 198 199 200 201 |
# File 'lib/foodcritic/api.rb', line 189 def find_resources(ast, = {}) = { type: :any }.merge!() return [] unless ast.respond_to?(:xpath) scope_type = "" unless [:type] == :any type_array = Array([:type]).map { |x| "@value='#{x}'" } scope_type = "[#{type_array.join(' or ')}]" end # TODO: Include nested resources (provider actions) no_actions = "[command/ident/@value != 'action']" ast.xpath("//method_add_block[command/ident#{scope_type}]#{no_actions}") end |
#gem_version(version) ⇒ Object
Helper to return a comparable version for a string.
204 205 206 |
# File 'lib/foodcritic/api.rb', line 204 def gem_version(version) Gem::Version.create(version) end |
#included_recipes(ast, options = { with_partial_names: true }) ⇒ Object
Retrieve the recipes that are included within the given recipe AST.
These two usages are equivalent:
included_recipes(ast)
included_recipes(ast, :with_partial_names => true)
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 |
# File 'lib/foodcritic/api.rb', line 215 def included_recipes(ast, = { with_partial_names: true }) raise_unless_xpath!(ast) filter = ["[count(descendant::args_add) = 1]"] # If `:with_partial_names` is false then we won't include the string # literal portions of any string that has an embedded expression. unless [:with_partial_names] filter << "[count(descendant::string_embexpr) = 0]" end string_desc = '[descendant::args_add/string_literal]/ descendant::tstring_content' included = ast.xpath([ "//command[ident/@value = 'include_recipe']", "//fcall[ident/@value = 'include_recipe']/ following-sibling::arg_paren", ].map do |recipe_include| recipe_include + filter.join + string_desc end.join(" | ")) # Hash keyed by recipe name with matched nodes. included.inject(Hash.new([])) { |h, i| h[i["value"]] += [i]; h } end |
#json_file_to_hash(filename) ⇒ Hash
Give a filename path it returns the hash of the JSON contents
431 432 433 434 435 436 437 438 439 440 |
# File 'lib/foodcritic/api.rb', line 431 def json_file_to_hash(filename) raise "File #{filename} not found" unless File.exist?(filename) file = File.read(filename) begin FFI_Yajl::Parser.parse(file) rescue FFI_Yajl::ParseError raise "File #{filename} does not appear to contain valid JSON" end end |
#literal_searches(ast) ⇒ Object
Searches performed by the specified recipe that are literal strings. Searches with a query formed from a subexpression will be ignored.
242 243 244 245 246 |
# File 'lib/foodcritic/api.rb', line 242 def literal_searches(ast) return [] unless ast.respond_to?(:xpath) ast.xpath("//method_add_arg[fcall/ident/@value = 'search' and count(descendant::string_embexpr) = 0]/descendant::tstring_content") end |
#match(node) ⇒ Object
Create a match from the specified node.
249 250 251 252 253 254 255 |
# File 'lib/foodcritic/api.rb', line 249 def match(node) raise_unless_xpath!(node) pos = node.xpath("descendant::pos").first return nil if pos.nil? { matched: node.respond_to?(:name) ? node.name : "", line: pos["line"].to_i, column: pos["column"].to_i } end |
#metadata_field(file, field) ⇒ String
Retrieves a value of a metadata field.
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/foodcritic/api.rb', line 71 def (file, field) until (file.split(File::SEPARATOR) & standard_cookbook_subdirs).empty? file = File.absolute_path(File.dirname(file.to_s)) end file = File.dirname(file) unless File.extname(file).empty? md_path = File.join(file, "metadata.rb") if File.exist?(md_path) value = read_ast(md_path).xpath("//stmts_add/ command[ident/@value='#{field}']/ descendant::tstring_content/@value").to_s raise "Cant read #{field} from #{md_path}" if value.to_s.empty? return value else raise "Cant find #{md_path}" end end |
#read_ast(file) ⇒ Object
Read the AST for the given Ruby source file
269 270 271 |
# File 'lib/foodcritic/api.rb', line 269 def read_ast(file) ast_cache[file] ||= uncached_read_ast(file) end |
#resource_attribute(resource, name) ⇒ Object
Retrieve a single-valued attribute from the specified resource.
274 275 276 277 |
# File 'lib/foodcritic/api.rb', line 274 def resource_attribute(resource, name) raise ArgumentError, "Attribute name cannot be empty" if name.empty? resource_attributes(resource)[name.to_s] end |
#resource_attributes(resource, options = {}) ⇒ Object
Retrieve all attributes from the specified resource.
280 281 282 283 284 285 286 287 |
# File 'lib/foodcritic/api.rb', line 280 def resource_attributes(resource, = {}) atts = {} name = resource_name(resource, ) atts[:name] = name unless name.empty? atts.merge!(normal_attributes(resource, )) atts.merge!(block_attributes(resource)) atts end |
#resource_attributes_by_type(ast) ⇒ Object
Resources keyed by type, with an array of matching nodes for each.
290 291 292 293 294 295 296 297 298 |
# File 'lib/foodcritic/api.rb', line 290 def resource_attributes_by_type(ast) result = {} resources_by_type(ast).each do |type, resources| result[type] = resources.map do |resource| resource_attributes(resource) end end result end |
#resource_name(resource, options = {}) ⇒ Object
Retrieve the name attribute associated with the specified resource.
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 |
# File 'lib/foodcritic/api.rb', line 301 def resource_name(resource, = {}) raise_unless_xpath!(resource) = { return_expressions: false }.merge() if [:return_expressions] name = resource.xpath("command/args_add_block") if name.xpath("descendant::string_add").size == 1 && name.xpath("descendant::string_literal").size == 1 && name.xpath( "descendant::*[self::call or self::string_embexpr]").empty? name.xpath("descendant::tstring_content/@value").to_s else name end else # Preserve existing behaviour resource.xpath("string(command//tstring_content/@value)") end end |
#resource_type(resource) ⇒ Object
Return the type, e.g. ‘package’ for a given resource
331 332 333 334 335 336 337 338 |
# File 'lib/foodcritic/api.rb', line 331 def resource_type(resource) raise_unless_xpath!(resource) type = resource.xpath("string(command/ident/@value)") if type.empty? raise ArgumentError, "Provided AST node is not a resource" end type end |
#resources_by_type(ast) ⇒ Object
Resources in an AST, keyed by type.
321 322 323 324 325 326 327 328 |
# File 'lib/foodcritic/api.rb', line 321 def resources_by_type(ast) raise_unless_xpath!(ast) result = Hash.new { |hash, key| hash[key] = Array.new } find_resources(ast).each do |resource| result[resource_type(resource)] << resource end result end |
#ruby_code?(str) ⇒ Boolean
Does the provided string look like ruby code?
341 342 343 344 345 346 347 348 |
# File 'lib/foodcritic/api.rb', line 341 def ruby_code?(str) str = str.to_s return false if str.empty? checker = FoodCritic::ErrorChecker.new(str) checker.parse !checker.error? end |
#searches(ast) ⇒ Object
Searches performed by the provided AST.
351 352 353 354 |
# File 'lib/foodcritic/api.rb', line 351 def searches(ast) return [] unless ast.respond_to?(:xpath) ast.xpath("//fcall/ident[@value = 'search']") end |
#standard_cookbook_subdirs ⇒ array
The list of standard cookbook sub-directories.
360 361 362 363 |
# File 'lib/foodcritic/api.rb', line 360 def standard_cookbook_subdirs %w{attributes definitions files libraries providers recipes resources templates} end |
#supported_platforms(ast) ⇒ Array<Hash>
Platforms declared as supported in cookbook metadata. Returns an array of hashes containing the name and version constraints for each platform.
370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 |
# File 'lib/foodcritic/api.rb', line 370 def supported_platforms(ast) # Find the supports() method call. platforms_ast = field(ast, "supports") # Look for the first argument (the node next to the top args_new) and # filter out anything with a string_embexpr since that can't be parsed # statically. Then grab the static value for both strings and symbols, and # finally combine it with the word list (%w{}) analyzer. platforms = platforms_ast.xpath("(.//args_new)[1]/../*[not(.//string_embexpr)]").xpath(".//tstring_content|.//symbol/ident") | word_list_values(platforms_ast) platforms.map do |platform| # For each platform value, look for all arguments after the first, then # extract the string literal value. versions = platform.xpath("ancestor::args_add[not(args_new)]/*[position()=2]//tstring_content/@value") { platform: platform["value"].lstrip, versions: versions.map(&:to_s) } end.sort_by { |p| p[:platform] } end |
#template_file(resource) ⇒ Object
Template filename
387 388 389 390 391 392 393 394 395 396 397 |
# File 'lib/foodcritic/api.rb', line 387 def template_file(resource) if resource["source"] resource["source"] elsif resource[:name] if resource[:name].respond_to?(:xpath) resource[:name] else "#{File.basename(resource[:name])}.erb" end end end |
#template_paths(recipe_path) ⇒ Object
Templates in the current cookbook
416 417 418 419 420 421 422 423 |
# File 'lib/foodcritic/api.rb', line 416 def template_paths(recipe_path) Dir.glob(Pathname.new(recipe_path).dirname.dirname + "templates" + "**/*", File::FNM_DOTMATCH).select do |path| File.file?(path) end.reject do |path| File.basename(path) == ".DS_Store" || File.extname(path) == ".swp" end end |
#templates_included(all_templates, template_path, depth = 1) ⇒ Object
399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 |
# File 'lib/foodcritic/api.rb', line 399 def templates_included(all_templates, template_path, depth = 1) raise RecursedTooFarError.new(template_path) if depth > 10 partials = read_ast(template_path).xpath('//*[self::command or child::fcall][descendant::ident/@value="render"]//args_add/ string_literal//tstring_content/@value').map { |p| p.to_s } Array(template_path) + partials.map do |included_partial| partial_path = Array(all_templates).find do |path| (Pathname.new(template_path).dirname + included_partial).to_s == path end if partial_path Array(partial_path) + templates_included(all_templates, partial_path, depth + 1) end end.flatten.uniq.compact end |