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 i{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 |
# 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 file = File.dirname(file) until (Dir.entries(file) & %w{metadata.rb metadata.json}).any? file end |
#cookbook_maintainer(file) ⇒ String
Return metadata maintainer property given any file in the cookbook
108 109 110 111 112 |
# File 'lib/foodcritic/api.rb', line 108 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
118 119 120 121 122 |
# File 'lib/foodcritic/api.rb', line 118 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.
90 91 92 93 94 95 96 97 98 99 100 101 102 |
# File 'lib/foodcritic/api.rb', line 90 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 file = File.absolute_path(File.dirname(file.to_s)) until (file.split(File::SEPARATOR) & standard_cookbook_subdirs).empty? file = File.dirname(file) unless File.extname(file).empty? File.basename(file) end end |
#declared_dependencies(ast) ⇒ Object
The dependencies declared in cookbook metadata.
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
# File 'lib/foodcritic/api.rb', line 125 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.
150 151 152 153 154 155 156 |
# File 'lib/foodcritic/api.rb', line 150 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
159 160 161 162 163 |
# File 'lib/foodcritic/api.rb', line 159 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(&: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.
167 168 169 170 171 |
# File 'lib/foodcritic/api.rb', line 167 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))
186 187 188 189 190 191 192 193 194 195 196 197 198 199 |
# File 'lib/foodcritic/api.rb', line 186 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.
202 203 204 |
# File 'lib/foodcritic/api.rb', line 202 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)
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 |
# File 'lib/foodcritic/api.rb', line 213 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
436 437 438 439 440 441 442 443 444 445 |
# File 'lib/foodcritic/api.rb', line 436 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.
240 241 242 243 244 245 |
# File 'lib/foodcritic/api.rb', line 240 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.
248 249 250 251 252 253 254 255 |
# File 'lib/foodcritic/api.rb', line 248 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.
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/foodcritic/api.rb', line 69 def (file, field) file = File.absolute_path(File.dirname(file.to_s)) until (file.split(File::SEPARATOR) & standard_cookbook_subdirs).empty? 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 278 |
# 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.
281 282 283 284 285 286 287 288 |
# File 'lib/foodcritic/api.rb', line 281 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.
291 292 293 294 295 296 297 298 299 |
# File 'lib/foodcritic/api.rb', line 291 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.
302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 |
# File 'lib/foodcritic/api.rb', line 302 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
333 334 335 336 337 338 339 340 341 |
# File 'lib/foodcritic/api.rb', line 333 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.
323 324 325 326 327 328 329 330 |
# File 'lib/foodcritic/api.rb', line 323 def resources_by_type(ast) raise_unless_xpath!(ast) result = Hash.new { |hash, key| hash[key] = [] } 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?
344 345 346 347 348 349 350 351 |
# File 'lib/foodcritic/api.rb', line 344 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.
354 355 356 357 358 |
# File 'lib/foodcritic/api.rb', line 354 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.
364 365 366 367 |
# File 'lib/foodcritic/api.rb', line 364 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.
374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 |
# File 'lib/foodcritic/api.rb', line 374 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
391 392 393 394 395 396 397 398 399 400 401 |
# File 'lib/foodcritic/api.rb', line 391 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
421 422 423 424 425 426 427 428 |
# File 'lib/foodcritic/api.rb', line 421 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
403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 |
# File 'lib/foodcritic/api.rb', line 403 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(&: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 |