# class RDoc::Parser

### Constants

SITE

### Attributes

input_file_name[RW]
top_level[RW]

### Public Class Methods

new(top_level, file_name, content, options, stats) click to toggle source

called with the top level file

    # File lib/puppet/util/rdoc/parser.rb, line 32
    def initialize(top_level, file_name, content, options, stats)
      @options = options
      @stats   = stats
      @input_file_name = file_name
      @top_level = PuppetTopLevel.new(top_level)
      @progress = $stderr unless options.quiet
    end

### Public Instance Methods

document_class(name, klass, container) click to toggle source

create documentation for a class named `name`

    # File lib/puppet/util/rdoc/parser.rb, line 254
    def document_class(name, klass, container)
      Puppet.debug "rdoc: found new class #{name}"
      container, name = get_class_or_module(container, name)

      superclass = klass.parent
      superclass = "" if superclass.nil? or superclass.empty?

      @stats.num_classes += 1
      comment = klass.doc
      look_for_directives_in(container, comment) unless comment.empty?
      cls = container.add_class(PuppetClass, name, superclass)
      # it is possible we already encountered this class, while parsing some namespaces
      # from other classes of other files. But at that time we couldn't know this class superclass
      # so, now we know it and force it.
      cls.superclass = superclass
      cls.record_location(@top_level)

      # scan class code for include
      code = klass.code.children if klass.code.is_a?(Puppet::Parser::AST::ASTArray)
      code ||= klass.code
      unless code.nil?
        scan_for_include_or_require(cls, code)
        scan_for_realize(cls, code)
        scan_for_resource(cls, code) if Puppet.settings[:document_all]
      end

      cls.comment = comment
    rescue => detail
      raise Puppet::ParseError, "impossible to parse class '#{name}' in #{klass.file} at line #{klass.line}: #{detail}"
    end

document_define(name, define, container) click to toggle source

create documentation for a define

    # File lib/puppet/util/rdoc/parser.rb, line 311
    def document_define(name, define, container)
      Puppet.debug "rdoc: found new definition #{name}"
      # find superclas if any
      @stats.num_methods += 1

      # find the parent
      # split define name by :: to find the complete module hierarchy
      container, name = get_class_or_module(container,name)

      # build up declaration
      declaration = ""
      define.arguments.each do |arg,value|
        declaration << "\$#{arg}"
        unless value.nil?
          declaration << " => "
          case value
          when Puppet::Parser::AST::Leaf
            declaration << "'#{value.value}'"
          when Puppet::Parser::AST::ASTArray
            declaration << "[#{value.children.collect { |v| "'#{v}'" }.join(", ")}]"
          else
            declaration << "#{value.to_s}"
          end
        end
        declaration << ", "
      end
      declaration.chop!.chop! if declaration.size > 1

      # register method into the container
      meth =  AnyMethod.new(declaration, name)
      meth.comment = define.doc
      container.add_method(meth)
      look_for_directives_in(container, meth.comment) unless meth.comment.empty?
      meth.params = "( #{declaration} )"
      meth.visibility = :public
      meth.document_self = true
      meth.singleton = false
    rescue => detail
      raise Puppet::ParseError, "impossible to parse definition '#{name}' in #{define.file} at line #{define.line}: #{detail}"
    end

document_node(name, node, container) click to toggle source

create documentation for a node

    # File lib/puppet/util/rdoc/parser.rb, line 286
    def document_node(name, node, container)
      Puppet.debug "rdoc: found new node #{name}"
      superclass = node.parent
      superclass = "" if superclass.nil? or superclass.empty?

      comment = node.doc
      look_for_directives_in(container, comment) unless comment.empty?
      n = container.add_node(name, superclass)
      n.record_location(@top_level)

      code = node.code.children if node.code.is_a?(Puppet::Parser::AST::ASTArray)
      code ||= node.code
      unless code.nil?
        scan_for_include_or_require(n, code)
        scan_for_realize(n, code)
        scan_for_vardef(n, code)
        scan_for_resource(n, code) if Puppet.settings[:document_all]
      end

      n.comment = comment
    rescue => detail
      raise Puppet::ParseError, "impossible to parse node '#{name}' in #{node.file} at line #{node.line}: #{detail}"
    end

find_object_named(container, name) click to toggle source

Due to a bug in RDoc, we need to roll our own find_module_named The issue is that RDoc tries harder by asking the parent for a class/module of the name. But by doing so, it can mistakenly use a module of same name but from which we are not descendant.

    # File lib/puppet/util/rdoc/parser.rb, line 63
    def find_object_named(container, name)
      return container if container.name == name
      container.each_classmodule do |m|
        return m if m.name == name
      end
      nil
    end

get_class_or_module(container, name) click to toggle source

walk down the namespace and lookup/create container as needed

    # File lib/puppet/util/rdoc/parser.rb, line 72
    def get_class_or_module(container, name)

      # class ::A -> A is in the top level
      if name =~ /^::/
        container = @top_level
      end

      names = name.split('::')

      final_name = names.pop
      names.each do |name|
        prev_container = container
        container = find_object_named(container, name)
        container ||= prev_container.add_class(PuppetClass, name, nil)
      end
      [container, final_name]
    end

look_for_directives_in(context, comment) click to toggle source

#look_for_directives_in scans the current `comment` for RDoc directives

    # File lib/puppet/util/rdoc/parser.rb, line 456
    def look_for_directives_in(context, comment)
      preprocess = SM::PreProcess.new(@input_file_name, @options.rdoc_include)

      preprocess.handle(comment) do |directive, param|
        case directive
        when "stopdoc"
          context.stop_doc
          ""
        when "startdoc"
          context.start_doc
          context.force_documentation = true
          ""
        when "enddoc"
          #context.done_documenting = true
          #""
          throw :enddoc
        when "main"
          options = Options.instance
          options.main_page = param
          ""
        when "title"
          options = Options.instance
          options.title = param
          ""
        when "section"
          context.set_current_section(param, comment)
          comment.replace("") # 1.8 doesn't support #clear
          break
        else
          warn "Unrecognized directive '#{directive}'"
          break
        end
      end
      remove_private_comments(comment)
    end

parse_elements(container) click to toggle source

Traverse the AST tree and produce code-objects node that contains the documentation

    # File lib/puppet/util/rdoc/parser.rb, line 354
    def parse_elements(container)
      Puppet.debug "rdoc: scanning manifest"

      @known_resource_types.hostclasses.values.sort { |a,b| a.name <=> b.name }.each do |klass|
        name = klass.name
        if klass.file == @input_file_name
          unless name.empty?
            document_class(name,klass,container)
          else # on main class document vardefs
            code = klass.code.children if klass.code.is_a?(Puppet::Parser::AST::ASTArray)
            code ||= klass.code
            scan_for_vardef(container, code) unless code.nil?
          end
        end
      end

      @known_resource_types.definitions.each do |name, define|
        if define.file == @input_file_name
          document_define(name,define,container)
        end
      end

      @known_resource_types.nodes.each do |name, node|
        if node.file == @input_file_name
          document_node(name.to_s,node,container)
        end
      end
    end

parse_fact(container) click to toggle source

this is a poor man custom fact parser :-)

    # File lib/puppet/util/rdoc/parser.rb, line 394
    def parse_fact(container)
      comments = ""
      current_fact = nil
      File.open(@input_file_name) do |of|
        of.each do |line|
          # fetch comments
          if line =~ /^[ \t]*# ?(.*)$/
            comments += $1 + "\n"
          elsif line =~ /^[ \t]*Facter.add\(['"](.*?)['"]\)/
            current_fact = Fact.new($1,{})
            look_for_directives_in(container, comments) unless comments.empty?
            current_fact.comment = comments
            container.add_fact(current_fact)
            current_fact.record_location(@top_level)
            comments = ""
            Puppet.debug "rdoc: found custom fact #{current_fact.name}"
          elsif line =~ /^[ \t]*confine[ \t]*:(.*?)[ \t]*=>[ \t]*(.*)$/
            current_fact.confine = { :type => $1, :value => $2 } unless current_fact.nil?
          else # unknown line type
            comments =""
          end
        end
      end
    end

parse_plugins(container) click to toggle source

create documentation for plugins

    # File lib/puppet/util/rdoc/parser.rb, line 384
    def parse_plugins(container)
      Puppet.debug "rdoc: scanning plugin or fact"
      if @input_file_name =~ /\/facter\/[^\/]+\.rb$/
        parse_fact(container)
      else
        parse_puppet_plugin(container)
      end
    end

parse_puppet_plugin(container) click to toggle source

this is a poor man puppet plugin parser :-) it doesn’t extract doc nor desc :-(

    # File lib/puppet/util/rdoc/parser.rb, line 421
    def parse_puppet_plugin(container)
      comments = ""
      current_plugin = nil

      File.open(@input_file_name) do |of|
        of.each do |line|
          # fetch comments
          if line =~ /^[ \t]*# ?(.*)$/
            comments += $1 + "\n"
          elsif line =~ /^[ \t]*newfunction[ \t]*\([ \t]*:(.*?)[ \t]*,[ \t]*:type[ \t]*=>[ \t]*(:rvalue|:lvalue)\)/
            current_plugin = Plugin.new($1, "function")
            container.add_plugin(current_plugin)
            look_for_directives_in(container, comments) unless comments.empty?
            current_plugin.comment = comments
            current_plugin.record_location(@top_level)
            comments = ""
            Puppet.debug "rdoc: found new function plugins #{current_plugin.name}"
          elsif line =~ /^[ \t]*Puppet::Type.newtype[ \t]*\([ \t]*:(.*?)\)/
            current_plugin = Plugin.new($1, "type")
            container.add_plugin(current_plugin)
            look_for_directives_in(container, comments) unless comments.empty?
            current_plugin.comment = comments
            current_plugin.record_location(@top_level)
            comments = ""
            Puppet.debug "rdoc: found new type plugins #{current_plugin.name}"
          elsif line =~ /module Puppet::Parser::Functions/
            # skip
          else # unknown line type
            comments =""
          end
        end
      end
    end

remove_private_comments(comment) click to toggle source

    # File lib/puppet/util/rdoc/parser.rb, line 492
    def remove_private_comments(comment)
      comment.gsub!(/^#--.*?^#\+\+/m, '')
      comment.sub!(/^#--.*/m, '')
    end

resource_stmt_to_ref(stmt) click to toggle source

    # File lib/puppet/util/rdoc/parser.rb, line 243
    def resource_stmt_to_ref(stmt)
      type = stmt.type.split("::").collect { |s| s.capitalize }.join("::")
      title = stmt.title.is_a?(Puppet::Parser::AST::ASTArray) ? stmt.title.to_s.gsub(/\[(.*)\]/,'\1') : stmt.title.to_s

      param = stmt.params.children.collect do |p|
        {"name" => p.param, "value" => p.value.to_s}
      end
      PuppetResource.new(type, title, stmt.doc, param)
    end

scan() click to toggle source

main entry point

    # File lib/puppet/util/rdoc/parser.rb, line 41
    def scan
      environment = Puppet::Node::Environment.new
      @known_resource_types = environment.known_resource_types
      unless environment.known_resource_types.watching_file?(@input_file_name)
        Puppet.info "rdoc: scanning #{@input_file_name}"
        if @input_file_name =~ /\.pp$/
          @parser = Puppet::Parser::Parser.new(environment)
          @parser.file = @input_file_name
          @parser.parse.instantiate('').each do |type|
            @known_resource_types.add type
          end
        end
      end

      scan_top_level(@top_level)
      @top_level
    end

scan_for_include_or_require(container, code) click to toggle source

create documentation for include statements we can find in `code` and associate it with `container`

    # File lib/puppet/util/rdoc/parser.rb, line 167
    def scan_for_include_or_require(container, code)
      code = [code] unless code.is_a?(Array)
      code.each do |stmt|
        scan_for_include_or_require(container,stmt.children) if stmt.is_a?(Puppet::Parser::AST::ASTArray)

        if stmt.is_a?(Puppet::Parser::AST::Function) and ['include','require'].include?(stmt.name)
          stmt.arguments.each do |included|
            Puppet.debug "found #{stmt.name}: #{included}"
            container.send("add_#{stmt.name}",Include.new(included.to_s, stmt.doc))
          end
        end
      end
    end

scan_for_realize(container, code) click to toggle source

create documentation for realize statements we can find in `code` and associate it with `container`

    # File lib/puppet/util/rdoc/parser.rb, line 183
    def scan_for_realize(container, code)
      code = [code] unless code.is_a?(Array)
      code.each do |stmt|
        scan_for_realize(container,stmt.children) if stmt.is_a?(Puppet::Parser::AST::ASTArray)

        if stmt.is_a?(Puppet::Parser::AST::Function) and stmt.name == 'realize'
          stmt.arguments.each do |realized|
            Puppet.debug "found #{stmt.name}: #{realized}"
            container.add_realize(Include.new(realized.to_s, stmt.doc))
          end
        end
      end
    end

scan_for_resource(container, code) click to toggle source

create documentation for resources we can find in `code` and associate it with `container`

    # File lib/puppet/util/rdoc/parser.rb, line 213
    def scan_for_resource(container, code)
      code = [code] unless code.is_a?(Array)
      code.each do |stmt|
        scan_for_resource(container,stmt.children) if stmt.is_a?(Puppet::Parser::AST::ASTArray)

        if stmt.is_a?(Puppet::Parser::AST::Resource) and !stmt.type.nil?
          begin
            type = stmt.type.split("::").collect { |s| s.capitalize }.join("::")
            stmt.instances.each do |inst|
              title = inst.title.is_a?(Puppet::Parser::AST::ASTArray) ? inst.title.to_s.gsub(/\[(.*)\]/,'\1') : inst.title.to_s
              Puppet.debug "rdoc: found resource: #{type}[#{title}]"

              param = []
              inst.parameters.children.each do |p|
                res = {}
                res["name"] = p.param
                res["value"] = "#{p.value.to_s}" unless p.value.nil?

                param << res
              end

              container.add_resource(PuppetResource.new(type, title, stmt.doc, param))
            end
          rescue => detail
            raise Puppet::ParseError, "impossible to parse resource in #{stmt.file} at line #{stmt.line}: #{detail}"
          end
        end
      end
    end

scan_for_vardef(container, code) click to toggle source

create documentation for global variables assignements we can find in `code` and associate it with `container`

    # File lib/puppet/util/rdoc/parser.rb, line 199
    def scan_for_vardef(container, code)
      code = [code] unless code.is_a?(Array)
      code.each do |stmt|
        scan_for_vardef(container,stmt.children) if stmt.is_a?(Puppet::Parser::AST::ASTArray)

        if stmt.is_a?(Puppet::Parser::AST::VarDef)
          Puppet.debug "rdoc: found constant: #{stmt.name} = #{stmt.value}"
          container.add_constant(Constant.new(stmt.name.to_s, stmt.value.to_s, stmt.doc))
        end
      end
    end

scan_top_level(container) click to toggle source

create documentation for the top level `container`

    # File lib/puppet/util/rdoc/parser.rb, line 129
    def scan_top_level(container)
      # use the module README as documentation for the module
      comment = ""
      %w{README README.rdoc}.each do |rfile|
        readme = File.join(File.dirname(File.dirname(@input_file_name)), rfile)
        comment = File.open(readme,"r") { |f| f.read } if FileTest.readable?(readme)
      end
      look_for_directives_in(container, comment) unless comment.empty?

      # infer module name from directory
      name = split_module(@input_file_name)
      if name.nil?
        # skip .pp files that are not in manifests directories as we can't guarantee they're part
        # of a module or the global configuration.
        container.document_self = false
        return
      end

      Puppet.debug "rdoc: scanning for #{name}"

      container.module_name = name
      container.global=true if name == SITE

      @stats.num_modules += 1
      container, name  = get_class_or_module(container,name)
      mod = container.add_module(PuppetModule, name)
      mod.record_location(@top_level)
      mod.comment = comment

      if @input_file_name =~ /\.pp$/
        parse_elements(mod)
      elsif @input_file_name =~ /\.rb$/
        parse_plugins(mod)
      end
    end

split_module(path) click to toggle source

#split_module tries to find if `path` belongs to the module path if it does, it returns the module name, otherwise if we are sure it is part of the global manifest path, "__site__" is returned. And finally if this path couldn't be mapped anywhere, nil is returned.

    # File lib/puppet/util/rdoc/parser.rb, line 94
    def split_module(path)
      # find a module
      fullpath = File.expand_path(path)
      Puppet.debug "rdoc: testing #{fullpath}"
      if fullpath =~ /(.*)\/([^\/]+)\/(?:manifests|plugins|lib)\/.+\.(pp|rb)$/
        modpath = $1
        name = $2
        Puppet.debug "rdoc: module #{name} into #{modpath} ?"
        Puppet::Node::Environment.new.modulepath.each do |mp|
          if File.identical?(modpath,mp)
            Puppet.debug "rdoc: found module #{name}"
            return name
          end
        end
      end
      if fullpath =~ /\.(pp|rb)$/
        # there can be paths we don't want to scan under modules
        # imagine a ruby or manifest that would be distributed as part as a module
        # but we don't want those to be hosted under <site>
        Puppet::Node::Environment.new.modulepath.each do |mp|
          # check that fullpath is a descendant of mp
          dirname = fullpath
          previous = dirname
          while (dirname = File.dirname(previous)) != previous
            previous = dirname
            return nil if File.identical?(dirname,mp)
          end
        end
      end
      # we are under a global manifests
      Puppet.debug "rdoc: global manifests"
      SITE
    end