Login
gpcr/src/gpcr/census.cr
Login

[edit]

src/gpcr/census.cr @ trunk

compiler/crystal/syntax

require 'compiler/crystal/syntax'
scale = 0.75
fontscale = 1.500
box wid 2.796 ht 0.340 with .nw at (0.035, -0.035) fill 0xCBD5E1 color 0xCBD5E1
box wid 2.796 ht 0.340 with .nw at (0.000, 0.000) fill 0x7C83E6 color 0x7C83E6
box wid 0.300 ht 0.065 rad 0.018 at (0.370, -0.022) fill 0xffffff color 0xffffff
line from (0.140, 0.000) to (0.140, -0.340) color white
line from (2.656, 0.000) to (2.656, -0.340) color white
text "require 'compiler/crystal/syntax'" color white at (1.398, -0.170)

crystal source

require "compiler/crystal/syntax"

json block

{
  "kind": "require",
  "name": "compiler/crystal/syntax",
  "doc": null,
  "children": []
}

json

require 'json'
scale = 0.75
fontscale = 1.500
box wid 1.330 ht 0.340 with .nw at (0.035, -0.455) fill 0xCBD5E1 color 0xCBD5E1
box wid 1.330 ht 0.340 with .nw at (0.000, -0.420) fill 0x7C83E6 color 0x7C83E6
box wid 0.300 ht 0.065 rad 0.018 at (0.370, -0.443) fill 0xffffff color 0xffffff
line from (0.140, -0.420) to (0.140, -0.760) color white
line from (1.190, -0.420) to (1.190, -0.760) color white
text "require 'json'" color white at (0.665, -0.590)

crystal source

require "json"

json block

{
  "kind": "require",
  "name": "json",
  "doc": null,
  "children": []
}

../gpcr

require '../gpcr'
scale = 0.75
fontscale = 1.500
box wid 1.498 ht 0.340 with .nw at (0.035, -0.875) fill 0xCBD5E1 color 0xCBD5E1
box wid 1.498 ht 0.340 with .nw at (0.000, -0.840) fill 0x7C83E6 color 0x7C83E6
box wid 0.300 ht 0.065 rad 0.018 at (0.370, -0.863) fill 0xffffff color 0xffffff
line from (0.140, -0.840) to (0.140, -1.180) color white
line from (1.358, -0.840) to (1.358, -1.180) color white
text "require '../gpcr'" color white at (0.749, -1.010)

crystal source

require "../gpcr"

json block

{
  "kind": "require",
  "name": "../gpcr",
  "doc": null,
  "children": []
}

    enum LineKind
      Native
      GPCR
      Primitive
    end

Span

Span
scale = 0.75
fontscale = 1.500
box wid 0.800 ht 0.340 rad 0.035 with .nw at (0.035, -1.295) fill 0xCBD5E1 color 0xCBD5E1
box wid 0.800 ht 0.340 rad 0.035 with .nw at (0.000, -1.260) fill 0xFF6680 color 0xFF6680
box wid 0.300 ht 0.065 rad 0.018 at (0.370, -1.283) fill 0xffffff color 0xffffff
text "Span" color white at (0.400, -1.430)

crystal source

    @[GPCR::Record]
    record Span, kind : LineKind, line_start : Int32, line_end : Int32

json block

{
  "kind": "record",
  "name": "Span",
  "doc": null,
  "fields": [
    "kind : LineKind",
    "line_start : Int32",
    "line_end : Int32"
  ]
}

    private class ScanState
      getter spans = [] of Span
      property blocks = 0
      property primitives = 0
      property graphs = 0
      property unannotated = 0
    end

    struct FileStats
      getter gpcr_lines : Int32
      getter primitive_lines : Int32
      getter native_lines : Int32
      getter total_lines : Int32
      getter blocks : Int32
      getter primitives : Int32
      getter graphs : Int32
      getter unannotated : Int32

      def initialize(@gpcr_lines, @primitive_lines, @native_lines, @total_lines, @blocks, @primitives, @graphs, @unannotated)
      end

      def total_defs : Int32
        @blocks + @primitives + @graphs + @unannotated
      end

      def gpcr_pct : Float64
        @total_lines > 0 ? (@gpcr_lines.to_f * 100.0 / @total_lines).round(1) : 0.0
      end
    end

    def self.count_file(source : String) : FileStats
      parser = Crystal::Parser.new(source)
      ast = parser.parse
      ast_nodes = ast.is_a?(Crystal::Expressions) ? ast.expressions : [ast] of Crystal::ASTNode

      total_lines = source.lines.size
      state = ScanState.new
      scan_defs(ast_nodes, state)

      line_kinds = Array.new(total_lines, LineKind::Native)
      state.spans.each do |span|
        first = span.line_start.clamp(1, total_lines)
        last = span.line_end.clamp(first, total_lines)
        (first..last).each { |line_no| line_kinds[line_no - 1] = span.kind }
      end

      gpcr_lines = line_kinds.count(LineKind::GPCR)
      primitive_lines = line_kinds.count(LineKind::Primitive)
      native_lines = total_lines - gpcr_lines - primitive_lines

      FileStats.new(gpcr_lines, primitive_lines, native_lines, total_lines,
        state.blocks, state.primitives, state.graphs, state.unannotated)
    end

    private def self.scan_defs(ast_nodes : Array(Crystal::ASTNode), state : ScanState) : Nil
      pending = [] of Crystal::Annotation

      ast_nodes.each do |n|
        if n.is_a?(Crystal::Annotation)
          pending << n
        else
          inner = n.is_a?(Crystal::VisibilityModifier) ? n.exp : n
          if inner.is_a?(Crystal::Def)
            has_block = pending.any? { |a| a.path.to_s.ends_with?("Block") }
            has_prim = pending.any? { |a| a.path.to_s.ends_with?("Primitive") }
            has_graph = pending.any? { |a| a.path.to_s.ends_with?("Graph") }
            if has_block
              state.blocks += 1
              state.spans << Span.new(LineKind::GPCR, span_start(pending, inner), span_end(inner))
            elsif has_prim
              state.primitives += 1
              state.spans << Span.new(LineKind::Primitive, span_start(pending, inner), span_end(inner))
            elsif has_graph
              state.graphs += 1
              state.spans << Span.new(LineKind::GPCR, span_start(pending, inner), span_end(inner))
            else
              state.unannotated += 1
            end
          elsif inner.is_a?(Crystal::Assign) && pending.any? { |a| a.path.to_s.ends_with?("Constant") || a.path.to_s.ends_with?("TableLiteral") }
            from = pending.first?.try(&.location).try(&.line_number) || inner.location.try(&.line_number) || 1
            to_loc = inner.end_location.try(&.line_number) || from
            state.blocks += 1
            state.spans << Span.new(LineKind::GPCR, from, to_loc)
          elsif inner.is_a?(Crystal::ClassDef) && pending.any? { |a| a.path.to_s.ends_with?("TableSchema") }
            from = pending.first?.try(&.location).try(&.line_number) || inner.location.try(&.line_number) || 1
            to_loc = inner.end_location.try(&.line_number) || from
            state.blocks += 1
            state.spans << Span.new(LineKind::GPCR, from, to_loc)
          elsif inner.is_a?(Crystal::Call) && inner.name == "record" && pending.any? { |a| a.path.to_s.ends_with?("Record") }
            from = pending.first?.try(&.location).try(&.line_number) || inner.location.try(&.line_number) || 1
            to_loc = inner.end_location.try(&.line_number) || from
            state.blocks += 1
            state.spans << Span.new(LineKind::GPCR, from, to_loc)
          else
            scan_defs(body_nodes(n), state)
          end
          pending = [] of Crystal::Annotation
        end
      end
    end

    private def self.body_nodes(node : Crystal::ASTNode) : Array(Crystal::ASTNode)
      case node
      when Crystal::Expressions
        node.expressions
      when Crystal::ModuleDef
        body_list(node.body)
      when Crystal::ClassDef
        body_list(node.body)
      else
        [] of Crystal::ASTNode
      end
    end

    private def self.body_list(node : Crystal::ASTNode) : Array(Crystal::ASTNode)
      case node
      when Crystal::Expressions then node.expressions
      when Crystal::Nop         then [] of Crystal::ASTNode
      else                           [node] of Crystal::ASTNode
      end
    end

    private def self.span_start(annotations : Array(Crystal::Annotation), node : Crystal::Def) : Int32
      annotations.first?.try(&.location).try(&.line_number) ||
        node.location.try(&.line_number) ||
        1
    end

    private def self.span_end(node : Crystal::Def) : Int32
      node.end_location.try(&.line_number) ||
        node.location.try(&.line_number) ||
        1
    end

    def self.census(files : Array(String)) : {Hash(String, FileStats), FileStats}
      file_stats = {} of String => FileStats
      total_gpcr_lines = 0
      total_primitive_lines = 0
      total_native_lines = 0
      total_lines = 0
      total_blocks = 0
      total_primitives = 0
      total_graphs = 0
      total_unannotated = 0

      files.each do |path|
        source = File.read(path)
        stats = count_file(source)
        file_stats[File.basename(path)] = stats
        total_gpcr_lines += stats.gpcr_lines
        total_primitive_lines += stats.primitive_lines
        total_native_lines += stats.native_lines
        total_lines += stats.total_lines
        total_blocks += stats.blocks
        total_primitives += stats.primitives
        total_graphs += stats.graphs
        total_unannotated += stats.unannotated
      end

      totals = FileStats.new(total_gpcr_lines, total_primitive_lines, total_native_lines, total_lines, total_blocks, total_primitives, total_graphs, total_unannotated)
      {file_stats, totals}
    end

    def self.census_sources(pairs : Array({String, String})) : {Hash(String, FileStats), FileStats}
      file_stats = {} of String => FileStats
      total_gpcr_lines = 0
      total_primitive_lines = 0
      total_native_lines = 0
      total_lines = 0
      total_blocks = 0
      total_primitives = 0
      total_graphs = 0
      total_unannotated = 0
      pairs.each do |(name, source)|
        stats = count_file(source)
        file_stats[name] = stats
        total_gpcr_lines += stats.gpcr_lines
        total_primitive_lines += stats.primitive_lines
        total_native_lines += stats.native_lines
        total_lines += stats.total_lines
        total_blocks += stats.blocks
        total_primitives += stats.primitives
        total_graphs += stats.graphs
        total_unannotated += stats.unannotated
      end
      totals = FileStats.new(total_gpcr_lines, total_primitive_lines, total_native_lines, total_lines, total_blocks, total_primitives, total_graphs, total_unannotated)
      {file_stats, totals}
    end

    def self.report_json(file_stats : Hash(String, FileStats), totals : FileStats) : String
      JSON.build do |j|
        j.object do
          j.field "files" do
            j.object do
              file_stats.each do |filename, stats|
                j.field filename do
                  j.object do
                    j.field "gpcr_lines", stats.gpcr_lines
                    j.field "primitive_lines", stats.primitive_lines
                    j.field "native_lines", stats.native_lines
                    j.field "total_lines", stats.total_lines
                    j.field "gpcr_pct", stats.gpcr_pct
                    j.field "blocks", stats.blocks
                    j.field "primitives", stats.primitives
                    j.field "graphs", stats.graphs
                    j.field "unannotated", stats.unannotated
                    j.field "total_defs", stats.total_defs
                  end
                end
              end
            end
          end
          j.field "project" do
            j.object do
              j.field "gpcr_lines", totals.gpcr_lines
              j.field "primitive_lines", totals.primitive_lines
              j.field "native_lines", totals.native_lines
              j.field "total_lines", totals.total_lines
              j.field "gpcr_pct", totals.gpcr_pct
              j.field "blocks", totals.blocks
              j.field "primitives", totals.primitives
              j.field "graphs", totals.graphs
              j.field "unannotated", totals.unannotated
              j.field "total_defs", totals.total_defs
            end
          end
        end
      end
    end