O TEMA DO FÓRUM ESTÁ EM MANUTENÇÃO. FEEDBACKS AQUI: ACESSAR

RGSSDoc - Gerador de Documentação de scripts do RGSS

Iniciado por Brandt, 30/07/2019 às 21:31

30/07/2019 às 21:31 Última edição: 16/03/2020 às 00:20 por Brandt


[box class=pagelinks send_topic]
Gerador de Documentação de scripts do RGSS
por Masked


#invernoCRM

Veja também: RGSS3 CLI
[/box]


[box class=plainbox]Introdução[/box]

TL;DR: Fiz um script que lê os scripts do RPG Maker VX Ace e gera documentação em markdown/HTML baseado nos comentários.

Oi povo \o

Esses dias dei uma fuçada e trabalhei um pouco nesse script aqui, e gostaria de compartilhar com vocês porque achei que o resultado ficou bem bacana. Também espero que possa ajudar quem puder se interessar, e principalmente facilitar um pouco do desenvolvimento de scripts pra RPG Maker VX Ace.

Como quem já mexer com o RGSS3 deve saber, o maker vem com uma documentação das classes internas do RGSS (i.e. Sprite, Bitmap, Window, Graphics, Input, etc.), mas não existe nenhum documento acessível para os scripts padrão (i.e. Scene_Base, Window_Base, Window_Command, etc.) além dos comentários que vêm nos scripts.
Mas é meio maçante ficar rondando as classes procurando a função certa para o que queremos, e isso leva a duplicação de código (não achei uma função que faça isso, então vou fazer a minha!), gambiarras (Window_Command? Nah, vou usar a Window_Selectable e boto umas coisas em cima, vai dar bom) e deus sabe mais lá o que.

A ideia aqui é usar esses comentários que vêm nas classes (que são razoavelmente bem formatados, por sorte xd) e gerar documentos com base neles, pra ficar mais acessível a indexação das funções de cada classe e pra quê cada uma serve.

Já aviso, porém, que meu tédio era grande, e posso ter exagerado um teco na implementação disso kk
Mas prossigamos...


[box class=plainbox]Como funciona?[/box]

O funcionamento do script é o seguinte: usando a variável global mágica $RGSS_SCRIPTS, o script itera sobre o código de todos os scripts no projeto (isso inclui scripts adicionais, mais sobre isso à frente). Para cada script encontrado, o RGSSDoc cria um arquivo markdown na pasta especificada ({PROJETO}/docs, por padrão) e, através de análise do código e dos comentários presentes no script, cria seções no arquivo para as classes e módulos daquele script, documentando os métodos privados e de instância usando também os comentários deles.

O formato de comentário que o script reconhece é o que vem nos scripts do RM por padrão, ou seja, para classes e módulos:
#==============================================================================
# ** Classe
#------------------------------------------------------------------------------
#  Esta classe faz tal, tal e tal coisa.
#==============================================================================


E para métodos:
#--------------------------------------------------------------------------
# * Método que faz X
#   arg1: Primeiro argumento, serve pra Y
#--------------------------------------------------------------------------


O resto do processamento independe do formato dos comentários, e leva em conta apenas a sintaxe do próprio Ruby, mas é importante que os comentários nos scripts sigam esse formato. Comentários não formatados são desconsiderados.

No futuro, possivelmente eu devo adicionar formas de customizar esse formato.

Bom, como esse é um tópico de exposição de programação, acho que é interessante também comentar sobre a forma como a análise do código dos scripts é feita.


[box class=plainbox]Por trás das cenas[/box]

Basicamente, o script é um analisador léxico e sintático bem incompleto de Ruby. Incompleto porque ele só lida com a sintaxe necessária para identificar o que é uma classe ou módulo e o que é um método, e onde cada coisa começa e acaba, além de associar um comentário para cada uma dessas coisas.

A arquitetura do analisador é bem padrão, e segue um modelo assim:

[box class=pagelinks quoteheader]
Fonte: Guru99[/box]

Basicamente, o que acontece é que o código passa por um Lexer, que divide ele em "palavras", e esse lexer alimenta um Parser, que organiza essas palavras em sentenças de código que fazem sentido, baseado na sintaxe da linguagem. Dessa forma, podemos transformar um código que é um monte de caracteres em uma estrutura de dados que conseguimos usar para gerar significado. Esse processo é basicamente a mesma coisa que acontece em compiladores e interpretadores das linguagens que você vê por aí.

Nota do autor: Sinceramente, para o propósito desse script, isso não era totalmente necessário. De toda forma, sempre curti muito compiladores e tal e achei bem interessante desenvolver um desse pra Ruby xd
No pior dos casos, serve pra alguém que no futuro queira tentar fazer um interpretador de alguma natureza baseado nisso, quem sabe?


[box class=plainbox]O script[/box]

Sem mais delongas, eis o código:

#==============================================================================
# RGSS Doc | v0.2.1 | por Masked
#
# para RPG Maker VX Ace
#------------------------------------------------------------------------------
# Gera automaticamente documentação para todos os scripts do jogo e salva em
# uma pasta pré-definida (./docs por padrão) no formato Markdown.
#==============================================================================
#==============================================================================
# ** RGSSDoc
#------------------------------------------------------------------------------
#  Este módulo concentra as funções de interface com a geração de documentação
# e serve como namespace para os demais módulos do script.
#==============================================================================
module RGSSDoc
  #--------------------------------------------------------------------------
  # * Pasta onde serão salvos os documentos
  #--------------------------------------------------------------------------
  OUTPUT_FOLDER = './docs'
  #--------------------------------------------------------------------------
  # * Gera documentação para um script
  #   name  : Nome do script
  #   code  : Código do script
  #--------------------------------------------------------------------------
  def self.generate(name, code)
    doc = ScriptDoc.new(name, code)
    name.gsub!(/[\x00\/\\:\*\?\"<>\|]/, '')
    doc.save_doc File.join(OUTPUT_FOLDER, "#{name}.md")
    doc.save_reference_graph File.join(OUTPUT_FOLDER, "#{name}.dot")
  end
end
#==============================================================================
# ** RGSSDoc::Lexer
#------------------------------------------------------------------------------
#  Esta classe processa um código Ruby e divide em tokens.
#==============================================================================
class RGSSDoc::Lexer
  #--------------------------------------------------------------------------
  # * Atributos
  #--------------------------------------------------------------------------
  attr_reader :index
  #--------------------------------------------------------------------------
  # * Constantes
  #--------------------------------------------------------------------------
  OPERATORS = %w[& && || | + < > << >> ! = ^ - / * ** %]
  OPERATORS_EQUALS = OPERATORS.map { |op| op + '='}
  MISC = %w[\[ \] \[\] { } ( ) <=> =~ :: ~ ? : , . ;]
  SYMBOLS = MISC + OPERATORS + OPERATORS_EQUALS
  #--------------------------------------------------------------------------
  # * Inicialização do objeto
  #   code  : Código
  #--------------------------------------------------------------------------
  def initialize(code)
    @code = code
    @word = false
    reset
  end
  #--------------------------------------------------------------------------
  # * Reseta a iteração
  #   index : Posição inicial da iteração
  #--------------------------------------------------------------------------
  def reset(index = 0)
    @index = index
  end
  #--------------------------------------------------------------------------
  # * Itera sobre os tokens no código
  #   start : Posição inicial no código
  #--------------------------------------------------------------------------
  def tokens(start = 0)
    token = ''
    TokenEnumerator.new(self) do |enum|
      reset(start)
      while @index < @code.size
        next_token(enum)
      end
      enum.yield Token.new(@index, :eos, :eof)
    end
  end
  #--------------------------------------------------------------------------
  # * Limpa e envia a token para o enumerador
  #   start : Posição de início da token atual
  #   enum  : Enumerador de tokens
  #   token : Token
  #--------------------------------------------------------------------------
  def flush(start, enum, token)
    return false if token.empty?
    @word = true
    number = token =~ /^\d/
    enum.yield(Token.new(start, number ? :number : :word, token)) if enum
    true
  end
  #--------------------------------------------------------------------------
  # * Avança um token no código
  #   enum  : Enumerador de tokens
  #--------------------------------------------------------------------------
  def next_token(enum)
    token = ''
    start = nil
    word = @word
    @word = false
    while @index < @code.size
      char = @code[@index]
      if char == '#'
        return if flush(start, enum, token)
        return comment(enum)
      elsif char =~ /["']/
        return if flush(start, enum, token)
        return string(enum)
      elsif char =~ /[\n;]/
        return if flush(start, enum, token)
        @index += 1
        return enum.yield Token.new(@index - 1, :eos, char)
      elsif char =~ /\s/
        return if flush(start, enum, token)
      elsif char == '/' and not word
        @word = true
        return string(enum)
      elsif char == '%' and not word
        @word = true
        return percent_string(enum)
      elsif (char =~ /[?!]/ and ((not word and token.empty?) or (@index > 0 and @code[@index - 1] =~ /\s/))) or char =~ /[\[\]\{\}\(\)=\-\+\/%\*\.;,\|&<>\:]/
        return if flush(start, enum, token)
        return symbol(enum)
      elsif char =~ /[?!]/
        token << char
        @index += 1
        return flush(start, enum, token)
      else
        start ||= @index
        token << char
      end
      @index += 1
    end
    flush(start, enum, token)
  end
  #--------------------------------------------------------------------------
  # * Processa uma token de comentário
  #   enum  : Enumerador de tokens
  #--------------------------------------------------------------------------
  def comment(enum)
    while @code[@index] == '#'
      start = @index + 1
      @index += 1 until @index == @code.size or @code[@index] == "\n"
      if enum
        enum.yield Token.new(start, :comment, @code[start...@index].strip)
      end
      @index += 1
      @index += 1 while @code[@index] =~ /\s/
    end
    return if @index >= @code.size
    @index -= 1 until @code[@index] == "\n"
  end
  #--------------------------------------------------------------------------
  # * Avança a leitura do código para o fim de uma string
  #   end_mark  : Símbolo de fim da string
  #   simple    : Se a string é simples (sem interpolação)
  #--------------------------------------------------------------------------
  def find_string_end(end_mark, simple = false)
    escape = false
    @index += 1
    until @index >= @code.size or (@code[@index] == end_mark and not escape)
      if not simple and @code[@index, 2] == '#{' and not escape
        skip_string_interpolation
        next
      else
        escape = (not escape and @code[@index] == "\\")
      end
      @index += 1
    end
  end
  #--------------------------------------------------------------------------
  # * Processa uma token de string
  #   enum  : Enumerador de tokens
  #--------------------------------------------------------------------------
  def string(enum)
    mark = @code[@index]
    start = @index + 1
    find_string_end(mark, mark == "'")
    @index += 1
    return unless enum
    enum.yield Token.new(start - 1, :string, @code[start...@index - 1])
  end
  #--------------------------------------------------------------------------
  # * Processa uma token de expressão %...
  #   enum  : Enumerador de tokens
  #--------------------------------------------------------------------------
  def percent_string(enum)
    @index += 1
    @index += 1 if @code[@index] =~ /[iqrswx]/
    mark = @code[@index]
    if not mark =~ /[\(\[\{\<]/
      end_mark = mark
    elsif mark == '('
      end_mark = ')'
    else
      end_mark = (mark.ord + 2).chr
    end
    start = @index + 1
    find_string_end(end_mark)
    @index += 1
    return unless enum
    enum.yield Token.new(start - 1, :string, @code[start...@index - 1])
  end
  #--------------------------------------------------------------------------
  # * Pula interpolação de string
  #--------------------------------------------------------------------------
  def skip_string_interpolation
    @index += 2
    depth = 1
    enum = Enumerator.new do |enum|
      next_token(enum) until depth.zero?
    end
    for token in enum
      depth -= 1 if token.value == '}'
    end
  end
  #--------------------------------------------------------------------------
  # * Processa uma token de símbolo
  #   enum  : Enumerador de tokens
  #--------------------------------------------------------------------------
  def symbol(enum)
    token = @code[@index]
    start = @index
    @index += 1
    while @index < @code.size and SYMBOLS.include?(token + @code[@index])
      token << @code[@index]
      @index += 1
    end
    @word = %w<) ] }>.include?(token)
    return unless enum
    enum.yield Token.new(start, :symbol, token)
  end
end
#==============================================================================
# ** RGSSDoc::BasicToken
#------------------------------------------------------------------------------
#  Esta classe representa tokens de modo geral.
#==============================================================================
class RGSSDoc::BasicToken
  #--------------------------------------------------------------------------
  # * Atributos
  #--------------------------------------------------------------------------
  attr_reader   :index
  attr_reader   :type
  attr_reader   :value
  #--------------------------------------------------------------------------
  # * Inicialização do objeto
  #   index : Posição da token no código
  #   type  : Tipo de token
  #   value : Texto da token
  #--------------------------------------------------------------------------
  def initialize(index, type, value)
    @index = index
    @type = type
    @value = value
  end
  #--------------------------------------------------------------------------
  # * Converte a token em string
  #--------------------------------------------------------------------------
  alias inspect to_s
  #--------------------------------------------------------------------------
  # * Converte a token em string
  #--------------------------------------------------------------------------
  def to_s
    "<#{self.class}:#{@type}@#{@index} #{@value.inspect}>"
  end
end
#==============================================================================
# ** RGSSDoc::Lexer::Token
#------------------------------------------------------------------------------
#  Esta classe representa tokens de código Ruby.
#==============================================================================
class RGSSDoc::Lexer::Token < RGSSDoc::BasicToken
  #--------------------------------------------------------------------------
  # * Constantes
  #--------------------------------------------------------------------------
  BLOCK_KEYWORDS = %w(begin case for class def do if module unless until while)
  OTHER_KEYWORDS = %w(alias and break next defined? else elsif ensure rescue
                      false in nil not or redo retry super self then return
                      true undef yield end when)
  #--------------------------------------------------------------------------
  # * Verifica se a token é uma palavra
  #--------------------------------------------------------------------------
  def word?
    type == :word
  end
  #--------------------------------------------------------------------------
  # * Verifica se a token é um símbolo
  #--------------------------------------------------------------------------
  def symbol?
    type == :symbol
  end
  #--------------------------------------------------------------------------
  # * Verifica se a token é um fim de sequência
  #--------------------------------------------------------------------------
  def eos?
    type == :eos
  end
  #--------------------------------------------------------------------------
  # * Verifica se a token é um comentário
  #--------------------------------------------------------------------------
  def comment?
    type == :comment
  end
  #--------------------------------------------------------------------------
  # * Verifica se a token é um nome de variável
  #--------------------------------------------------------------------------
  def name?
    word? and not keyword?
  end
  #--------------------------------------------------------------------------
  # * Verifica se a token é uma constante
  #--------------------------------------------------------------------------
  def const?
    name? and value[0] =~ /[A-Z]/
  end
  #--------------------------------------------------------------------------
  # * Verifica se a token é uma variável de instância
  #--------------------------------------------------------------------------
  def instance_variable?
    name? and value[0] == /@/
  end
  #--------------------------------------------------------------------------
  # * Verifica se a token é uma variável global
  #--------------------------------------------------------------------------
  def global_variable?
    name? and value[0] == /$/
  end
  #--------------------------------------------------------------------------
  # * Verifica se a token é uma variável
  #--------------------------------------------------------------------------
  def variable?
    name? and not const?
  end
  #--------------------------------------------------------------------------
  # * Verifica se a token é uma palavra chave
  #--------------------------------------------------------------------------
  def keyword?
    return true if block?
    return OTHER_KEYWORDS.include?(value)
  end
  #--------------------------------------------------------------------------
  # * Verifica se a token é uma keyword de bloco
  #--------------------------------------------------------------------------
  def block?(last = :eos)
    return false unless word?
    return false unless BLOCK_KEYWORDS.include?(value)
    return false unless last == :eos or value == 'do'
    true
  end
end
#==============================================================================
# ** RGSSDoc::Lexer::TokenEnumerator
#------------------------------------------------------------------------------
#  Esta classe implementa um enumerador de tokens. Inclui funções úteis para
# leitura das tokens de forma estruturada.
#==============================================================================
class RGSSDoc::Lexer::TokenEnumerator < Enumerator
  #--------------------------------------------------------------------------
  # * Atributos
  #--------------------------------------------------------------------------
  attr_reader   :lexer
  #--------------------------------------------------------------------------
  # * Inicialização do objeto
  #   lexer : Lexer associado ao enumerador
  #   block : Bloco de iteração
  #--------------------------------------------------------------------------
  def initialize(lexer, &block)
    super(&block)
    @lexer = lexer
  end
  #--------------------------------------------------------------------------
  # * Avança a iteração até a próxima token do tipo EOS
  #--------------------------------------------------------------------------
  def skip_to_eos
    token = nil
    loop do
      token = self.next
      break if token.eos?
    end
    lexer.reset(token.index)
  end
  #--------------------------------------------------------------------------
  # * Pula uma token caso ela exista, não faz nada se não
  #--------------------------------------------------------------------------
  def skip(type, value = nil)
    token = self.next
    return token unless token.type == type
    return self.next if token.value == value or value.nil?
    token
  end
end
#==============================================================================
# ** RGSSDoc::Parser
#------------------------------------------------------------------------------
#  Esta classe processa blocos de código Ruby para composição da documentação.
#==============================================================================
class RGSSDoc::Parser
  #--------------------------------------------------------------------------
  # * Atributos
  #--------------------------------------------------------------------------
  attr_reader   :lexer
  #--------------------------------------------------------------------------
  # * Inicialização do objeto
  #   code  : Código
  #--------------------------------------------------------------------------
  def initialize(code)
    @code = code
    @lexer = RGSSDoc::Lexer.new(code)
  end
  #--------------------------------------------------------------------------
  # * Reseta a iteração
  #--------------------------------------------------------------------------
  def reset
    @lexer.reset
  end
  #--------------------------------------------------------------------------
  # * Itera sobre as sentenças no código
  #   start : Posição inicial no código
  #--------------------------------------------------------------------------
  def statements(start = 0)
    Enumerator.new do |enum|
      comment = []
      last = :eos
      for token in @lexer.tokens(start)
        if token.block?(last)
          @lexer.reset(token.index)
          skip_code_block
          text = @code[start...@lexer.index]
          enum.yield Statement.new(start, :block, text, comment.join("\r\n"))
          comment.clear
          start = @lexer.index
        elsif token.comment? and (last == :eos or last == :comment)
          comment << token.value
          start = @lexer.index
        elsif token.eos?
          text = @code[start...@lexer.index] || ''
          unless text.strip.empty?
            enum.yield Statement.new(start, :simple, text, comment.join("\r\n"))
            comment.clear
          end
          start = @lexer.index
        end
        last = token.type
      end
    end
  end
  #--------------------------------------------------------------------------
  # * Lê um bloco de código terminado em end.
  #--------------------------------------------------------------------------
  def skip_code_block
    depth = 0
    last = :eos
    has_while = false
    for token in @lexer.tokens(@lexer.index)
      has_while ||= ['while', 'until'].include?(token.value)
      if token.word?
        if token.block?(last) and not (token.value == 'do' and has_while)
          depth += 1
        elsif token.value == 'end'
          depth -= 1
        end
      elsif token.eos?
        has_while = false
      end
      last = token.type
      break if depth.zero?
    end
  end
end
#==============================================================================
# ** RGSSDoc::Parser::Statement
#------------------------------------------------------------------------------
#  Esta classe representa sentenças de código Ruby. Uma sentença é o menor 
# bloco de código com significado, que pode ser executado independente do
# código ao seu redor sem apresentar erros de sintaxe.
#==============================================================================
class RGSSDoc::Parser::Statement < RGSSDoc::BasicToken
  #--------------------------------------------------------------------------
  # * Atributos
  #--------------------------------------------------------------------------
  attr_reader   :comment
  #--------------------------------------------------------------------------
  # * Inicialização do objeto
  #   index   : Posição da token no código
  #   type    : Tipo de token
  #   value   : Texto da token
  #   comment : Comentário
  #--------------------------------------------------------------------------
  def initialize(index, type, value, comment)
    super(index, type, value)
    @comment = comment.force_encoding('utf-8')
  end
  #--------------------------------------------------------------------------
  # * Verifica se a sentença é um bloco de código
  #--------------------------------------------------------------------------
  def block?
    type == :block
  end
  #--------------------------------------------------------------------------
  # * Primeira token da sentença
  #--------------------------------------------------------------------------
  def tokens
    @lexer ||= RGSSDoc::Lexer.new(value)
    @lexer.tokens
  end
  #--------------------------------------------------------------------------
  # * Lê o cabeçalho da sentença.
  #--------------------------------------------------------------------------
  def head
    return value unless type == :block
    return @head if @head
    tokens.skip_to_eos
    @head = value[0...@lexer.index].strip
  end
  #--------------------------------------------------------------------------
  # * Lê o código interno da sentença.
  #--------------------------------------------------------------------------
  def inner_code
    return value unless type == :block
    return @inner if @inner
    head
    start = @lexer.index
    depth = 0
    has_while = false
    last = :eos
    for token in @lexer.tokens(start)
      if token.word?
        has_while ||= token.value == 'while'
        if token.block?(last) and not (token.value == 'do' and has_while)
          depth += 1
        elsif token.value == 'end'
          depth -= 1
          if depth == -1
            @lexer.reset(token.index)
            break
          end
        end
      elsif token.eos?
        has_while = false
      end
      last = token.type
    end
    @inner = value[start...@lexer.index]
  end
end
#==============================================================================
# ** RGSSDoc::ScriptDoc
#------------------------------------------------------------------------------
#  Esta classe representa um script sendo documentado.
#==============================================================================
class RGSSDoc::ScriptDoc
  #--------------------------------------------------------------------------
  # * Atributos
  #--------------------------------------------------------------------------
  attr_reader   :name
  attr_reader   :code
  attr_reader   :modules
  #--------------------------------------------------------------------------
  # * Inicialização do objeto
  #   name  : Nome do script
  #   code  : Código do script
  #--------------------------------------------------------------------------
  def initialize(name, code)
    @name = name
    @code = code
    parse_modules
  end
  #--------------------------------------------------------------------------
  # * Processa o código e extrai os módulos do script
  #--------------------------------------------------------------------------
  def parse_modules
    @modules = []
    for m in RGSSDoc::ModuleDoc.parse(code)
      @modules << m
    end
  end
  #--------------------------------------------------------------------------
  # * Salva a documentação
  #   file  : Nome do arquivo de destino
  #--------------------------------------------------------------------------
  def save_doc(file)
    directory = File.dirname(file)
    Dir.mkdir(directory) unless FileTest.directory?(directory)
    File.open(file, 'w') do |file|
      file.puts "# #{name}"
      for m in modules
        file.puts "## `#{m.name}` <small style='color: grey;'>(#{m.type})</small>"
        file.puts
        file.puts "Herda de `#{m.super_class}`\r\n" if m.super_class
        
        static_methods = m.methods.select(&:static?)
        unless static_methods.empty?
          file.puts '### Métodos estáticos'
          file.puts
          file.puts '<table>'
          file.puts '<thead>'
          file.puts '<tr><th>Nome</th><th>Descrição</th><th>Argumentos</th></tr>'
          file.puts '</thead>'
          file.puts '<tbody>'
          for method in static_methods
            file.print '<tr>'
            file.print "<td><code>#{method.name}</code></td>"
            file.print "<td>#{method.description}</td>"
            file.print '<td><ul>'
            for a in method.arguments
              file.print '<li>'
              file.print "<code>#{a.name}</code>: #{a.description}"
              file.print '</li>'
            end
            file.print '</ul></td>'
            file.puts '</tr>'
          end
          file.puts '</tbody>'
          file.puts '</table>'
          file.puts
        end
        
        instance_methods = m.methods.reject(&:static?)
        unless instance_methods.empty?
          file.puts '### Métodos de instância'
          file.puts
          file.puts '<table>'
          file.puts '<thead>'
          file.puts '<tr><th>Nome</th><th>Descrição</th><th>Argumentos</th></tr>'
          file.puts '</thead>'
          file.puts '<tbody>'
          for method in instance_methods
            file.print '<tr>'
            file.print "<td><code>#{method.name}</code></td>"
            file.print "<td>#{method.description}</td>"
            file.print '<td><ul>'
            for a in method.arguments
              file.print '<li>'
              file.print "<code>#{a.name}</code>: #{a.description}"
              file.print '</li>'
            end
            file.print '</ul></td>'
            file.puts '</tr>'
          end
          file.puts '</tbody>'
          file.puts '</table>'
          file.puts
        end
        
        file.puts
        file.puts '---'
      end
    end
  end
  #--------------------------------------------------------------------------
  # * Salva o grafo de referências das classes do script
  #   file  : Nome do arquivo de destino
  #--------------------------------------------------------------------------
  def save_reference_graph(file)
    File.open(file, 'w') do |file|
      file.puts "digraph \"#{name}\" {"
      for m in @modules
        for ref in m.references
          file.puts "\t\"#{m.name}\" -> \"#{ref}\""
        end
        next if (m.super_class || '').empty?
        file.print "\"#{m.name}\" -> \"#{m.super_class}\""
        file.puts " [style=dotted label=super]"
      end
      file.puts "}"
    end
  end
end
#==============================================================================
# ** RGSSDoc::RubyEntityDoc
#------------------------------------------------------------------------------
#  Esta classe representa uma entidade sendo documentada. Uma entidade é
# um módulo, classe ou método.
#==============================================================================
class RGSSDoc::RubyEntityDoc
  #--------------------------------------------------------------------------
  # * Atributos
  #--------------------------------------------------------------------------
  attr_reader   :statement
  #--------------------------------------------------------------------------
  # * Inicialização do objeto
  #   statement : Sentença de declaração do módulo
  #--------------------------------------------------------------------------
  def initialize(statement)
    @statement = statement
  end
  #--------------------------------------------------------------------------
  # * Código da entidade
  #--------------------------------------------------------------------------
  def code
    statement.value
  end
  #--------------------------------------------------------------------------
  # * Descrição da entidade
  #--------------------------------------------------------------------------
  def description
    raise NotImplementedError
  end
end
#==============================================================================
# ** RGSSDoc::ModuleDoc
#------------------------------------------------------------------------------
#  Esta classe representa um módulo sendo documentado.
#==============================================================================
class RGSSDoc::ModuleDoc < RGSSDoc::RubyEntityDoc
  #--------------------------------------------------------------------------
  # * Atributos
  #--------------------------------------------------------------------------
  attr_reader   :submodules
  attr_reader   :methods
  #--------------------------------------------------------------------------
  # * Inicialização do objeto
  #   statement : Sentença de declaração do módulo
  #--------------------------------------------------------------------------
  def initialize(statement)
    super
    parse_submodules
    parse_methods
  end
  #--------------------------------------------------------------------------
  # * Nome do módulo
  #--------------------------------------------------------------------------
  def name
    return @name if @name
    tokens = statement.tokens
    tokens.next
    token = tokens.skip(:symbol, '<<')
    parts = []
    loop do
      break unless token.word?
      parts << token.value
      token = tokens.skip(:symbol, '::')
    end
    @name = parts.join('::')
  end
  #--------------------------------------------------------------------------
  # * Tipo do módulo (class/module)
  #--------------------------------------------------------------------------
  def type
    statement.tokens.first.value
  end
  #--------------------------------------------------------------------------
  # * Verifica se o módulo é uma classe
  #--------------------------------------------------------------------------
  def class?
    type == 'class'
  end
  #--------------------------------------------------------------------------
  # * Superclasse
  #--------------------------------------------------------------------------
  def super_class
    return @super_class if @super_class
    tokens = statement.tokens
    loop do
      token = tokens.next
      return nil if token.eos?
      break if token.symbol? and token.value == '<'
    end
    parts = []
    loop do
      token = tokens.next
      next if token.symbol? and token.value == '::'
      break unless token.word?
      parts << token.value
    end
    @super_class = parts.join('::')
  end
  #--------------------------------------------------------------------------
  # * Lista de referências do módulo
  #--------------------------------------------------------------------------
  def references
    begin
      @methods.flat_map do |method|
        method.references(eval(self.name))
      end.uniq
    rescue NameError
      []
    end
  end
  #--------------------------------------------------------------------------
  # * Descrição do módulo
  #--------------------------------------------------------------------------
  def description
    statement.comment =~ /\s*===+\s*\n\*\* \S+\s*\n---+(.+)\s*\n===+/m
    return $1.strip.gsub(/\r?\n/, '') if $1
    nil
  end
  #--------------------------------------------------------------------------
  # * Processa o código do módulo e extrai os submódulos declarados nele
  #--------------------------------------------------------------------------
  def parse_submodules
    @submodules = []
    for m in RGSSDoc::ModuleDoc.parse(statement.inner_code)
      @submodules << m
    end
  end
  #--------------------------------------------------------------------------
  # * Processa o código do módulo e extrai os métodos declarados nele
  #--------------------------------------------------------------------------
  def parse_methods
    @methods = []
    for m in RGSSDoc::MethodDoc.parse(statement.inner_code)
      @methods << m
    end
  end
  #--------------------------------------------------------------------------
  # * Processa código e extrai os módulos contidos nele
  #   code  : Código do script
  #--------------------------------------------------------------------------
  def self.parse(code)
    Enumerator.new do |enum|
      doc = RGSSDoc::Parser.new(code)
      for statement in doc.statements
        token = statement.tokens.first
        next unless token.word?
        next unless ['module', 'class'].include?(token.value)
        enum.yield(RGSSDoc::ModuleDoc.new(statement))
      end
    end
  end
end
#==============================================================================
# ** RGSSDoc::MethodDoc
#------------------------------------------------------------------------------
#  Esta classe representa um método sendo documentado.
#==============================================================================
class RGSSDoc::MethodDoc < RGSSDoc::RubyEntityDoc
  #--------------------------------------------------------------------------
  # * Verifica se o método é estático
  #--------------------------------------------------------------------------
  def static?
    tokens = statement.tokens
    tokens.next
    token = tokens.next
    token.word? and token.value == 'self'
  end
  #--------------------------------------------------------------------------
  # * Nome do método
  #--------------------------------------------------------------------------
  def name
    tokens = statement.tokens
    tokens.next
    token = tokens.next
    if token.word? and token.value == 'self'
      tokens.next
      token = tokens.next
    end
    token.value
  end
  #--------------------------------------------------------------------------
  # * Lista de referências do método
  #--------------------------------------------------------------------------
  def references(scope = Kernel)
    Enumerator.new do |enum|
      parser = RGSSDoc::Parser.new(statement.inner_code)
      for statement in parser.statements
        tokens = statement.tokens
        begin
          loop do
            token = tokens.next
            next unless token.const?
            name = token.value
            token = tokens.next
            while token.symbol? and token.value == '::'
              token = tokens.next
              next 2 unless token.const?
              name << "::" << token.value
              token = tokens.next
            end
            begin
              x = scope.module_eval(name)
              is_module = x.is_a?(Module)
              next unless is_module
              enum.yield(x.name)
            rescue NameError => e
            end
          end
        rescue StopIteration
        end
      end
    end.to_a.uniq
  end
  #--------------------------------------------------------------------------
  # * Descrição do método
  #--------------------------------------------------------------------------
  def description
    statement.comment =~ /\s*---+\s*\n\* ([^\n]+)\s*.*\n---+/m
    return $1.strip.gsub(/\r?\n/, '') if $1
    nil
  end
  #--------------------------------------------------------------------------
  # * Argumentos documentados do método
  #--------------------------------------------------------------------------
  def arguments
    statement.comment =~ /\s*---+\s*\n\* [^\n]+\s*(.*)\n---+/m
    return {} unless $1
    doc = $1
    arg = nil
    args = []
    for line in doc.split(/(?:\r?\n)+/)
      if line =~ /(\S+)\s*:\s*(.+)/
        args << arg if arg
        arg = ArgumentDoc.new($1, $2.strip)
      elsif arg
        arg.description << ' ' + line.strip
      end
    end
    args << arg if arg
    return args
  end
  #--------------------------------------------------------------------------
  # * Processa código e extrai os métodos contidos nele
  #   code  : Código do script
  #--------------------------------------------------------------------------
  def self.parse(code)
    Enumerator.new do |enum|
      doc = RGSSDoc::Parser.new(code)
      for statement in doc.statements
        token = statement.tokens.first
        next unless token.word?
        next unless token.value == 'def'
        enum.yield(RGSSDoc::MethodDoc.new(statement))
      end
    end
  end
end
#==============================================================================
# ** RGSSDoc::MethodDoc::ArgumentDoc
#------------------------------------------------------------------------------
#  Esta classe representa um argumento de um método sendo documentado.
#==============================================================================
class RGSSDoc::MethodDoc::ArgumentDoc
  #--------------------------------------------------------------------------
  # * Atributos
  #--------------------------------------------------------------------------
  attr_reader   :name
  attr_accessor :description
  #--------------------------------------------------------------------------
  # * Inicialização do objeto
  #   name        : Nome do argumento
  #   description : Descrição do argumento
  #--------------------------------------------------------------------------
  def initialize(name, description)
    @name = name
    @description = description
  end
  #--------------------------------------------------------------------------
  # * Converte o objeto em string
  #--------------------------------------------------------------------------
  def to_s
    "#{name} : #{description}"
  end
end
#==============================================================================
# ** Main
#------------------------------------------------------------------------------
#  Lê cada script do jogo e gera documentação para ele de acordo.
#==============================================================================
for id, name, compressed, code in $RGSS_SCRIPTS
  next if name.empty? or code.strip.empty?
  RGSSDoc.generate(name, code)
end


Ao final do script existe um bloco "Main" que itera sobre os scripts do jogo e salva documentação para os que for possível. Veja que esse processo é meio demoradinho (Ruby não é lá a linguagem mais rápida, e minha implementação está longe de otimizada também), então recomendo rodar apenas uma vez.

Você pode desativar o script bem fácil adicionando um __END__ antes da primeira linha (o código no editor de scripts do RMVXAce deve ficar todo preto).


[box class=plainbox]Metas para o futuro[/box]


  • Implementar documentação para scripts adicionais de forma mais interessante (possivelmente ler os comentários no começo do script pra gerar informações como autor, data, versão, instruções, etc.)
  • Tratar melhor os argumentos dos métodos (atualmente a documentação é gerada totalmente baseada no comentário, mas dá pra tirar algumas coisas da definição do método também)
  • Gerar grafos de dependência entre as classes, por exemplo, se minha classe Scene_Title cria um objeto do tipo Window_CommandTitle, mostrar isso como uma ligação entre as duas classes. Já tenho em mente mais ou menos como fazer isso, mas vai dar mais algum trampo.
  • Documentar atributos
  • Lidar com modificadores de acesso (private/protected/public) pra não documentar coisas privadas
  • Documentar aliases
~ Masked

Pô, níveis consideráveis de bruxaria nisso. E não ficou lento. Levou menos de dez segundos, considerando que é um script pra uso único - ou muito raro - está mais do que bom. Os arquivos estão todos separadinhos, todos muito bem formatados. O código em si também está bem bacana, eu que não programo entendi.

Qual a possibilidade disso gerar uma documentação propriamente dita? Tipo a do MV, que já abre no navegador normalmente.

Poste uma aula pra nós sobre regex. ,_,

Citação de: Corvo online 31/07/2019 às 21:37
Pô, níveis consideráveis de bruxaria nisso. E não ficou lento. Levou menos de dez segundos, considerando que é um script pra uso único - ou muito raro - está mais do que bom. Os arquivos estão todos separadinhos, todos muito bem formatados. O código em si também está bem bacana, eu que não programo entendi.

Qual a possibilidade disso gerar uma documentação propriamente dita? Tipo a do MV, que já abre no navegador normalmente.

Poste uma aula pra nós sobre regex. ,_,

É meio bruxoso mesmo, tanto que tenho quase certeza que tem algumas coisas que tão quebradas aí no meio ainda xd

Quanto à velocidade, penso mais ou menos o mesmo, mas acho que tem espaço pra melhora. Se eu salvar um hash de cada script pra só processar ele na primeira vez e quando for alterado ainda dá pra dar um jeito de deixar ele lá sem ter que desativar, talvez valha a pena tentar :sera:

Sobre gerar documentação propriamente dita, com certeza! É só questão de formatar direitinho os arquivos e depois gerar um índice pra linkar tudo. Existem até umas engines de site estático por aí que geram páginas a partir de markdown (o próprio Github, por exemplo, faz isso com Jekyll, no Github Pages).

hmmm, vou considerar :p




Atualização 0.2.0

  • Correção na análise de sintaxe para a barra (/), ponto de interrogação (?), while...do e outros.
  • Grafos de dependência de classes dentro do script. Além do arquivo .md, agora o script gera um arquivo .dot, que pode ser lido pelo GraphViz para gerar diagramas (experimente no navegador).

    Como exemplo, vou deixar o grafo que ele gerou para o próprio RGSSDoc:

    [box class=pagelinks quoteheader][/box]


    Legenda: Cada nó (bolinha) é uma classe. As arestas (setinhas) sólidas indicam que a classe que aponta usa a classe apontada, e as pontilhadas indicam que a primeira classe herda da outra.

    Em scripts com muitas classes e relações isso fica meio bagunçado, ainda preciso estudar formas de deixar esses grafos melhor organizados. De toda forma, é uma ferramenta interessante pra análise de dependências e de estrutura dos scripts no geral, na minha opinião xd
~ Masked

Opa isso é bem legal hahah, agora tenho que sair usando em tudo que já criei na minha vida hsuahs.

Esses relacionamentos sempre são bom para saber se estamos arquitetando corretamente e o que estamos colocando em cada script. Parabéns pelos níveis de bruxaria xD, esse tipo de coisa é sempre genial!

Citação de: Raizen online 31/07/2019 às 23:19
Opa isso é bem legal hahah, agora tenho que sair usando em tudo que já criei na minha vida hsuahs.

Esses relacionamentos sempre são bom para saber se estamos arquitetando corretamente e o que estamos colocando em cada script. Parabéns pelos níveis de bruxaria xD, esse tipo de coisa é sempre genial!

ahuehauehaeuheah

Fiquei um tempo brincando de gerar grafos de scripts aleatórios aqui, é viciante. Recomendo.




Versão 0.2.1

  • Correção do bug que gerava erro quando scripts tinham caracteres especiais no nome
  • Correção do bug que gerava loop infinito quando há um comentário no fim da última linha do script
~ Masked