attractor 2.7.2 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/attractor/calculators/lizard_calculator.rb +59 -0
- data/lib/attractor/lizard.rb +79 -0
- data/lib/attractor/version.rb +1 -1
- data/lib/attractor.rb +1 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 48389ab1e67aacc626f598cbcaf4b899b95dc8bcacd1ca4403802981ec52cd72
|
|
4
|
+
data.tar.gz: f6366aa8f6b10cb75e235f2ce6216dea5597601da575eb7dcc16ab8311347757
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 699db99bb212c7a1aab465dacb7a2e9ce1796b18e5a44005fc0bd619a5baeb55cae9dd2c0a4750c5bb2b5f93232efc1b4d503df2cc92da6ee14a9a21e294ce6c
|
|
7
|
+
data.tar.gz: f7485127a0e46fcaae2bf809b1ad71d28cad9caebeaa70978d117d19a0d6a6d728121f96dfe1139a193a8a0921a493e5e65c67bab355ecb7b526a7192f50e3fe
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "attractor/lizard"
|
|
4
|
+
|
|
5
|
+
module Attractor
|
|
6
|
+
# Base calculator for languages scored with lizard. A plugin subclasses it and fixes the
|
|
7
|
+
# language and extension:
|
|
8
|
+
#
|
|
9
|
+
# class SwiftCalculator < LizardCalculator
|
|
10
|
+
# def initialize(**options)
|
|
11
|
+
# super(language: "swift", file_extension: "swift", **options)
|
|
12
|
+
# @type = "Swift"
|
|
13
|
+
# end
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# Complexity is the sum of the per-function cyclomatic complexity (McCabe, lizard's CCN),
|
|
17
|
+
# which is a different scale from flog's Ruby score; attractor reports per language, so
|
|
18
|
+
# the two are never summed. Details carry the same shape as attractor-ruby >= 0.4:
|
|
19
|
+
# `{ "name" => { "score" => ccn, "line" => start, "end_line" => end } }`.
|
|
20
|
+
class LizardCalculator < BaseCalculator
|
|
21
|
+
attr_reader :language
|
|
22
|
+
|
|
23
|
+
def initialize(language:, file_extension:, **options)
|
|
24
|
+
super(file_extension: file_extension, **options)
|
|
25
|
+
@language = language
|
|
26
|
+
@type = language.capitalize
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def calculate
|
|
30
|
+
super do |change|
|
|
31
|
+
functions = Lizard.analyze(change[:file_path], language: language)
|
|
32
|
+
[functions.sum(&:ccn), details_for(functions)]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# lizard reports bare function names without their type, so overloads and same-named
|
|
39
|
+
# methods in different types collide. A key must not depend on what else is in the file
|
|
40
|
+
# or on line numbers, or `diff` cannot pair a function with itself across refs; the
|
|
41
|
+
# signature (lizard's long name) is what actually differs between overloads, so it is
|
|
42
|
+
# the key on collision, and the start line only when even the signatures match.
|
|
43
|
+
def details_for(functions)
|
|
44
|
+
by_name = functions.group_by(&:name)
|
|
45
|
+
by_long_name = functions.group_by(&:long_name)
|
|
46
|
+
|
|
47
|
+
functions.each_with_object({}) do |fn, details|
|
|
48
|
+
key = if by_name[fn.name].size == 1
|
|
49
|
+
fn.name
|
|
50
|
+
elsif by_long_name[fn.long_name].size == 1
|
|
51
|
+
fn.long_name
|
|
52
|
+
else
|
|
53
|
+
"#{fn.long_name}@#{fn.start_line}"
|
|
54
|
+
end
|
|
55
|
+
details[key] = {"score" => fn.ccn, "line" => fn.start_line, "end_line" => fn.end_line}
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "csv"
|
|
4
|
+
require "open3"
|
|
5
|
+
|
|
6
|
+
module Attractor
|
|
7
|
+
# Runs lizard (https://github.com/terryyin/lizard), a multi-language cyclomatic complexity
|
|
8
|
+
# analyzer, and parses its CSV. Language plugins that have no native metric tool subclass
|
|
9
|
+
# LizardCalculator and pass their lizard language name; this module is the only place that
|
|
10
|
+
# knows how lizard is invoked.
|
|
11
|
+
module Lizard
|
|
12
|
+
Function = Struct.new(:name, :long_name, :ccn, :nloc, :start_line, :end_line)
|
|
13
|
+
|
|
14
|
+
INSTALL_HINT = "lizard is not installed. Install uv (https://docs.astral.sh/uv/) so `uvx lizard` works, " \
|
|
15
|
+
"or `pip install lizard`."
|
|
16
|
+
|
|
17
|
+
# nloc, ccn, tokens, params, length, location, file, name, long name, start, end
|
|
18
|
+
CSV_COLUMNS = 11
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
# Resolution order: a lizard on PATH (pip/pipx/brew), then uvx (fetches lizard on first run),
|
|
23
|
+
# then a clear error. Memoized per process; set ATTRACTOR_LIZARD to force a command.
|
|
24
|
+
# Windows note: this looks for the bare names, not PATHEXT variants; attractor is not
|
|
25
|
+
# tested there, and ATTRACTOR_LIZARD covers it.
|
|
26
|
+
def command
|
|
27
|
+
@command ||= begin
|
|
28
|
+
forced = ENV["ATTRACTOR_LIZARD"].to_s.split
|
|
29
|
+
if forced.any?
|
|
30
|
+
forced
|
|
31
|
+
elsif executable?("lizard")
|
|
32
|
+
["lizard"]
|
|
33
|
+
elsif executable?("uvx")
|
|
34
|
+
["uvx", "lizard"]
|
|
35
|
+
else
|
|
36
|
+
raise Attractor::Error, INSTALL_HINT
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def reset!
|
|
42
|
+
@command = nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def analyze(file_path, language:)
|
|
46
|
+
stdout, stderr, status = Open3.capture3(*command, "-l", language, "--csv", file_path)
|
|
47
|
+
raise Attractor::Error, "lizard failed on #{file_path}: #{stderr.strip}" unless status.success?
|
|
48
|
+
|
|
49
|
+
parse(stdout, file_path: file_path)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# `--csv` prints no header. Anything that is not a full row (a warning lizard writes to
|
|
53
|
+
# stdout, a blank line) is dropped rather than turned into a phantom function; a source
|
|
54
|
+
# file with bytes lizard echoes verbatim must not take the whole run down either.
|
|
55
|
+
def parse(csv, file_path: nil)
|
|
56
|
+
clean = csv.dup.force_encoding(Encoding::UTF_8).scrub("?")
|
|
57
|
+
|
|
58
|
+
CSV.parse(clean).filter_map do |row|
|
|
59
|
+
next unless row.size >= CSV_COLUMNS && integer?(row[1]) && integer?(row[9]) && integer?(row[10])
|
|
60
|
+
|
|
61
|
+
Function.new(row[7].to_s, row[8].to_s, row[1].to_i, row[0].to_i, row[9].to_i, row[10].to_i)
|
|
62
|
+
end
|
|
63
|
+
rescue CSV::MalformedCSVError => e
|
|
64
|
+
where = file_path ? " for #{file_path}" : nil
|
|
65
|
+
raise Attractor::Error, "lizard produced unreadable CSV#{where}: #{e.message}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def integer?(value)
|
|
69
|
+
value.to_s.match?(/\A-?\d+\z/)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def executable?(name)
|
|
73
|
+
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).any? do |dir|
|
|
74
|
+
candidate = File.join(dir, name)
|
|
75
|
+
File.file?(candidate) && File.executable?(candidate)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
data/lib/attractor/version.rb
CHANGED
data/lib/attractor.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "attractor/gem_names"
|
|
|
5
5
|
require "attractor/duration_parser"
|
|
6
6
|
require "attractor/config"
|
|
7
7
|
require "attractor/calculators/base_calculator"
|
|
8
|
+
require "attractor/calculators/lizard_calculator"
|
|
8
9
|
require "attractor/detectors/base_detector"
|
|
9
10
|
require "attractor/reporters/base_reporter"
|
|
10
11
|
require "attractor/suggester"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: attractor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Julian Rubisch
|
|
@@ -429,6 +429,7 @@ files:
|
|
|
429
429
|
- lib/attractor/cache.rb
|
|
430
430
|
- lib/attractor/calculators/base_calculator.rb
|
|
431
431
|
- lib/attractor/calculators/base_calculator.rb~
|
|
432
|
+
- lib/attractor/calculators/lizard_calculator.rb
|
|
432
433
|
- lib/attractor/cli.rb
|
|
433
434
|
- lib/attractor/cli.rb~
|
|
434
435
|
- lib/attractor/config.rb
|
|
@@ -446,6 +447,7 @@ files:
|
|
|
446
447
|
- lib/attractor/formatters/targets/diff.rb
|
|
447
448
|
- lib/attractor/gem_names.rb
|
|
448
449
|
- lib/attractor/git.rb
|
|
450
|
+
- lib/attractor/lizard.rb
|
|
449
451
|
- lib/attractor/registry_entry.rb
|
|
450
452
|
- lib/attractor/reporters/base_reporter.rb
|
|
451
453
|
- lib/attractor/reporters/base_reporter.rb~
|