Module: Tina4::Metrics

Defined in:
lib/tina4/metrics.rb

Overview

Thin dev-admin adapter for the native tina4 metrics engine (ADR-0054).

Constant Summary collapse

INSTALL_HINT =
'update the native tina4 CLI: https://tina4.com/cli'
SUMMARY_KEYS =
%w[files_analyzed total_functions avg_complexity avg_maintainability].freeze
FILE_KEYS =
%w[path loc avg_complexity maintainability has_tests].freeze
FUNCTION_KEYS =
%w[name file line complexity loc].freeze

Class Method Summary collapse

Class Method Details

.engine_pathObject



28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/tina4/metrics.rb', line 28

def self.engine_path
  ENV.fetch('PATH', '').split(File::PATH_SEPARATOR).each do |directory|
    %w[tina4 tina4.exe].each do |name|
      candidate = File.join(directory, name)
      next unless File.file?(candidate) && File.executable?(candidate)
      next if File.binread(candidate, 2) == '#!'

      return candidate
    rescue StandardError
      next
    end
  end
  nil
end

.file_detail(file_path) ⇒ Object

Raises:



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/tina4/metrics.rb', line 98

def self.file_detail(file_path)
  raise MetricsEngineError, 'file_detail needs a path' if file_path.to_s.empty?

  target = Pathname.new(file_path.to_s)
  target = Pathname.new(@last_scan_root).join(file_path.to_s) if !target.exist? && @last_scan_root
  raise MetricsEngineError, "no such file: #{file_path}" unless target.exist?
  raise MetricsEngineError, "not a file: #{file_path}" if target.directory?

  payload = run_engine(target.to_s)
  files = require_array(payload, 'file_metrics')
  raise MetricsEngineError, "engine reported no metrics for #{file_path}" if files.empty?

  files.first.merge(
    'function_count' => files.first.fetch('functions', 0),
    'functions' => require_array(payload, 'most_complex_functions'),
    'engine' => 'tina4-cli'
  )
end

.full_analysis(root = 'src') ⇒ Object

Raises:



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/tina4/metrics.rb', line 69

def self.full_analysis(root = 'src')
  resolved, scan_mode = resolve_target(root)
  payload = run_engine(resolved)
  summary = payload['summary']
  raise MetricsEngineError, "engine payload has no usable 'summary' - #{INSTALL_HINT}" unless summary.is_a?(Hash)

  files = require_array(payload, 'file_metrics')
  functions = require_array(payload, 'most_complex_functions')
  missing = SUMMARY_KEYS.reject { |key| summary.key?(key) }
  raise MetricsEngineError, "engine summary is missing #{missing.join(', ')}" unless missing.empty?
  unless files.empty?
    missing = FILE_KEYS.reject { |key| files.first.key?(key) }
    raise MetricsEngineError, "engine file_metrics is missing #{missing.join(', ')}" unless missing.empty?
  end
  unless functions.empty?
    missing = FUNCTION_KEYS.reject { |key| functions.first.key?(key) }
    raise MetricsEngineError, "engine function metrics are missing #{missing.join(', ')}" unless missing.empty?
  end

  SUMMARY_KEYS.to_h { |key| [key, summary[key]] }.merge(
    'file_metrics' => files,
    'most_complex_functions' => functions.first(15),
    'dependency_graph' => payload['dependency_graph'] || {},
    'scan_mode' => scan_mode,
    'scan_root' => resolved,
    'engine' => 'tina4-cli'
  )
end

.require_array(payload, key) ⇒ Object

Raises:



62
63
64
65
66
67
# File 'lib/tina4/metrics.rb', line 62

def self.require_array(payload, key)
  value = payload[key]
  return value if value.is_a?(Array)

  raise MetricsEngineError, "engine payload has no usable '#{key}' - #{INSTALL_HINT}"
end

.resolve_target(root = 'src') ⇒ Object



17
18
19
20
21
22
23
24
25
26
# File 'lib/tina4/metrics.rb', line 17

def self.resolve_target(root = 'src')
  source = Pathname.new(root)
  resolved, mode = if source.directory? && !Dir.glob(source.join('**/*.rb').to_s).empty?
                     [source.expand_path, 'project']
                   else
                     [Pathname.new(__dir__).expand_path, 'framework']
                   end
  @last_scan_root = resolved.to_s
  [resolved.to_s, mode]
end

.run_engine(path) ⇒ Object



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/tina4/metrics.rb', line 43

def self.run_engine(path)
  binary = engine_path
  raise MetricsEngineError, "tina4 not found on PATH - #{INSTALL_HINT}" unless binary

  stdout, stderr, status = Open3.capture3(binary, 'metrics', '--path', path.to_s, '--json')
  unless status.success?
    detail = (stderr.empty? ? stdout : stderr).strip.lines.first || "exit code #{status.exitstatus}"
    raise MetricsEngineError, "tina4 metrics failed on #{path}: #{detail.strip}"
  end
  payload = JSON.parse(stdout)
  raise MetricsEngineError, 'tina4 metrics returned a non-object payload' unless payload.is_a?(Hash)

  payload
rescue JSON::ParserError => error
  raise MetricsEngineError, "tina4 metrics returned unreadable JSON: #{error.message}"
rescue SystemCallError => error
  raise MetricsEngineError, "could not run #{binary}: #{error.message}"
end