klee 0.1.2 → 1.0.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/CHANGELOG.md +20 -19
- data/README.md +31 -3
- data/lib/klee/collaborators.rb +15 -25
- data/lib/klee/concept_index.rb +66 -3
- data/lib/klee/concepts.rb +27 -25
- data/lib/klee/file_analyzer.rb +104 -12
- data/lib/klee/gestalt.rb +3 -1
- data/lib/klee/profile.rb +33 -0
- data/lib/klee/version.rb +1 -1
- data/lib/klee/words.rb +20 -0
- data/lib/klee.rb +7 -4
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c8efdb5e1472295c0778a374828e8af7cb49e741a281d4d7558790863221ee0b
|
|
4
|
+
data.tar.gz: 99c547540505671159ace42750ba0ba6708aeb16f2888276a60426fc11316176
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4009d050d3439f95c2d451f7ec9a5c56f407c0fe95b269fb802335c2262886cb2bb0e159d71f7068aaa4f35c87836c366ee8745421d55e425e5791d110530df0
|
|
7
|
+
data.tar.gz: 68ffb23b66ae05b66874e677ca47bc3ed96274f681d9d0e9f3f1c84726db35851cb2f3657404690b80189284c8b1460a96d57339b9106175146f3200dae97016
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
|
6
6
|
and this project adheres to [Semantic Versioning](http://semver.org/).
|
|
7
7
|
|
|
8
|
+
## [1.0.0] - 2026-09-19
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Shared Words tokenizer with acronym splitting and predicate stripping (acff7d2)
|
|
13
|
+
- Nested class names, attr_* methods, and constant collaborators (18d27de)
|
|
14
|
+
- Klee.scan(profile: :rails) for models/lib globs and English stopwords (801fab2)
|
|
15
|
+
- ConceptIndex#catalog, #gaps, and #units (801fab2)
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Concepts#[] returns a Hash of concept words to method names (acff7d2)
|
|
20
|
+
- FileAnalyzer records each message receiver, not only locals and ivars (18d27de)
|
|
21
|
+
- Klee::Collaborators wraps FileAnalyzer instead of lexing the file (18d27de)
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Klee.concepts now forwards the modifiers: keyword (acff7d2)
|
|
26
|
+
- Operator messages, block params, and conversion chains are ignored (18d27de)
|
|
27
|
+
|
|
8
28
|
## [0.1.2] - 2026-02-01
|
|
9
29
|
|
|
10
30
|
### Added
|
|
@@ -20,22 +40,3 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|
|
20
40
|
### Changed
|
|
21
41
|
|
|
22
42
|
- README now includes MCP server setup and available tools (f84bdc5)
|
|
23
|
-
|
|
24
|
-
## [0.1.1] - 2026-01-29
|
|
25
|
-
|
|
26
|
-
### Changed
|
|
27
|
-
|
|
28
|
-
- Switch to supporting ruby 3.4 and up. (84fdd9c)
|
|
29
|
-
- Require ruby 3.4 and above (067ec87)
|
|
30
|
-
|
|
31
|
-
### Added
|
|
32
|
-
|
|
33
|
-
- Reissue for version and release support. (885fe28)
|
|
34
|
-
- SimpleCov for coverage reporting (889d169)
|
|
35
|
-
- Coverage comment from PRs (067ec87)
|
|
36
|
-
- Klee.scan() method for codebase-wide analysis with glob patterns (3e509a1)
|
|
37
|
-
- ConceptIndex for aggregating domain concepts across files (3e509a1)
|
|
38
|
-
- CollaboratorIndex for pairwise co-occurrence and clustering (3e509a1)
|
|
39
|
-
- FileAnalyzer using Prism AST to extract classes, methods, and collaborators (3e509a1)
|
|
40
|
-
- Configurable threshold and ignore list for filtering noise (3e509a1)
|
|
41
|
-
- Method-level scope option for tighter coupling analysis (3e509a1)
|
data/README.md
CHANGED
|
@@ -45,11 +45,14 @@ end
|
|
|
45
45
|
|
|
46
46
|
concept = Klee.object_concepts(Something)
|
|
47
47
|
|
|
48
|
-
concept[4]
|
|
49
|
-
|
|
48
|
+
concept[4]
|
|
49
|
+
# => { banana: [:banana_berry, :banana_boat, :banana_bunch, :banana_hammock], ... }
|
|
50
|
+
|
|
51
|
+
concept[3][:address]
|
|
52
|
+
# => [:address_informal_name, :address_street, :address_zip]
|
|
50
53
|
|
|
51
54
|
filtered = Klee.object_concepts(Something, modifiers: %i[fill_in_ hover_over_ _message])
|
|
52
|
-
filtered[4]
|
|
55
|
+
filtered[4] # modifiers stripped before words are counted
|
|
53
56
|
```
|
|
54
57
|
|
|
55
58
|
### Scan a codebase for domain concepts
|
|
@@ -77,6 +80,31 @@ codebase = Klee.scan("app/**/*.rb",
|
|
|
77
80
|
threshold: 3)
|
|
78
81
|
```
|
|
79
82
|
|
|
83
|
+
### Rails profile
|
|
84
|
+
|
|
85
|
+
Skip views, drop English function words, and get the extract-type read model:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
cb = Klee.scan(profile: :rails) # app/models/**/*.rb + lib/**/*.rb from cwd
|
|
89
|
+
|
|
90
|
+
cb.concepts.catalog.first
|
|
91
|
+
# => #<data Klee::ConceptIndex::Entry word="intensity" class_count=71 method_count=29 bias=:type>
|
|
92
|
+
|
|
93
|
+
cb.concepts.gaps
|
|
94
|
+
# => { "seconds" => { classes: 0, methods: 95, names: #<Set: {"trailing_rest_seconds", ...}> }, ... }
|
|
95
|
+
|
|
96
|
+
cb.concepts.units
|
|
97
|
+
# => { "seconds" => ["cap_clock_seconds", "trailing_rest_seconds", ...], "minutes" => [...], "kg" => [...] }
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`catalog` bias is `:type` (class-heavy), `:verb` (`build`/`calculate`/`extract`/…), `:gap` (many methods, almost no classes), or `:mixed`.
|
|
101
|
+
|
|
102
|
+
Pass explicit globs with the profile to keep the stopwords:
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
Klee.scan("app/models/**/*.rb", profile: :rails, threshold: 4)
|
|
106
|
+
```
|
|
107
|
+
|
|
80
108
|
### Find collaborator clusters
|
|
81
109
|
|
|
82
110
|
Discover which objects frequently work together across your codebase.
|
data/lib/klee/collaborators.rb
CHANGED
|
@@ -1,41 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "prism"
|
|
4
|
-
|
|
5
3
|
module Klee
|
|
6
4
|
class Collaborators
|
|
7
5
|
def initialize(const)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def lexed
|
|
13
|
-
Prism.lex_file(Object.const_source_location(const.name).first)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def additional_processing_types = [:DOT, :BRACKET_LEFT]
|
|
6
|
+
unless const.is_a?(Module)
|
|
7
|
+
raise ArgumentError, "expected a class or module, got #{const.inspect}"
|
|
8
|
+
end
|
|
17
9
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
additional_processing_types.include?(next_token.first.type)
|
|
22
|
-
token.first.value
|
|
23
|
-
end
|
|
10
|
+
path = Object.const_source_location(const.name)&.first
|
|
11
|
+
unless path && File.file?(path)
|
|
12
|
+
raise Error, "cannot locate source for #{const}"
|
|
24
13
|
end
|
|
14
|
+
|
|
15
|
+
@const = const
|
|
16
|
+
@analyzer = FileAnalyzer.new(path)
|
|
25
17
|
end
|
|
18
|
+
attr_reader :const
|
|
26
19
|
|
|
27
20
|
def tally
|
|
28
|
-
|
|
21
|
+
@analyzer.collaborators.tally
|
|
29
22
|
end
|
|
30
23
|
|
|
31
24
|
def rank
|
|
32
|
-
tally.group_by
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
end.reject do |key, value|
|
|
37
|
-
value.empty? || key < 3
|
|
38
|
-
end
|
|
25
|
+
tally.group_by { |_, count| count }
|
|
26
|
+
.sort.reverse.to_h
|
|
27
|
+
.transform_values { |value| value.map(&:first) }
|
|
28
|
+
.reject { |key, _value| key < 3 }
|
|
39
29
|
end
|
|
40
30
|
end
|
|
41
31
|
end
|
data/lib/klee/concept_index.rb
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "words"
|
|
4
|
+
|
|
3
5
|
module Klee
|
|
4
6
|
class ConceptIndex
|
|
5
7
|
include Enumerable
|
|
6
8
|
|
|
9
|
+
Entry = Data.define(:word, :class_count, :method_count, :class_names, :method_names, :bias)
|
|
10
|
+
|
|
11
|
+
VERBS = %w[
|
|
12
|
+
build calculate extract apply compute detect update create find get set
|
|
13
|
+
add ensure render save destroy validate serialize
|
|
14
|
+
parse process fetch refresh call fill infer display generate
|
|
15
|
+
normalize coerce prepare predict split sanitize mark
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
UNIT_PATTERNS = {
|
|
19
|
+
"seconds" => /_seconds\z/,
|
|
20
|
+
"minutes" => /_minutes\z/,
|
|
21
|
+
"kg" => /_kg\z/,
|
|
22
|
+
"m" => /_m\z/,
|
|
23
|
+
"reps" => /_reps\z/
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
7
26
|
def initialize(ignore: [], threshold: 2)
|
|
8
27
|
@ignore = Set.new(ignore.map(&:to_s))
|
|
9
28
|
@threshold = threshold
|
|
@@ -32,16 +51,60 @@ module Klee
|
|
|
32
51
|
filtered.sort_by { |word, locs| -(locs[:classes].size + locs[:methods].size) }.to_h
|
|
33
52
|
end
|
|
34
53
|
|
|
54
|
+
def catalog
|
|
55
|
+
rank.map do |word, locs|
|
|
56
|
+
Entry.new(
|
|
57
|
+
word: word,
|
|
58
|
+
class_count: locs[:classes].size,
|
|
59
|
+
method_count: locs[:methods].size,
|
|
60
|
+
class_names: locs[:classes],
|
|
61
|
+
method_names: locs[:methods],
|
|
62
|
+
bias: bias_for(word, locs)
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def gaps(min_methods: 8, max_classes: 3)
|
|
68
|
+
filtered.each_with_object({}) do |(word, locs), acc|
|
|
69
|
+
next if VERBS.include?(word)
|
|
70
|
+
next if locs[:methods].size < min_methods
|
|
71
|
+
next if locs[:classes].size > max_classes
|
|
72
|
+
|
|
73
|
+
acc[word] = {
|
|
74
|
+
classes: locs[:classes].size,
|
|
75
|
+
methods: locs[:methods].size,
|
|
76
|
+
names: locs[:methods]
|
|
77
|
+
}
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def units
|
|
82
|
+
names = @data.each_value.flat_map { |locs| locs[:methods].to_a }.uniq
|
|
83
|
+
UNIT_PATTERNS.each_with_object({}) do |(unit, pattern), acc|
|
|
84
|
+
matched = names.grep(pattern)
|
|
85
|
+
acc[unit] = matched.sort if matched.size >= @threshold
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
35
89
|
private
|
|
36
90
|
|
|
37
91
|
def words_from(name)
|
|
38
|
-
|
|
39
|
-
.downcase.split("_")
|
|
40
|
-
.reject { |w| w.empty? || @ignore.include?(w) }
|
|
92
|
+
Words.from(name, ignore: @ignore)
|
|
41
93
|
end
|
|
42
94
|
|
|
43
95
|
def filtered
|
|
44
96
|
@data.select { |_, locs| (locs[:classes].size + locs[:methods].size) >= @threshold }
|
|
45
97
|
end
|
|
98
|
+
|
|
99
|
+
def bias_for(word, locs)
|
|
100
|
+
return :verb if VERBS.include?(word)
|
|
101
|
+
|
|
102
|
+
class_count = locs[:classes].size
|
|
103
|
+
method_count = locs[:methods].size
|
|
104
|
+
return :type if class_count > method_count
|
|
105
|
+
return :gap if method_count >= 8 && class_count <= 3
|
|
106
|
+
|
|
107
|
+
:mixed
|
|
108
|
+
end
|
|
46
109
|
end
|
|
47
110
|
end
|
data/lib/klee/concepts.rb
CHANGED
|
@@ -1,26 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "words"
|
|
4
|
+
|
|
1
5
|
module Klee
|
|
2
6
|
class Concepts
|
|
3
7
|
include Enumerable
|
|
4
8
|
|
|
5
9
|
def initialize(*method_names, modifiers: [])
|
|
6
10
|
@method_names = method_names
|
|
7
|
-
@modifiers = modifiers
|
|
11
|
+
@modifiers = Array(modifiers).map(&:to_s)
|
|
8
12
|
end
|
|
9
13
|
attr_reader :method_names, :modifiers
|
|
10
14
|
|
|
11
15
|
def call(threshold)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
key
|
|
19
|
-
end
|
|
20
|
-
}
|
|
21
|
-
.compact
|
|
22
|
-
.transform_keys(&:to_sym)
|
|
23
|
-
.keys
|
|
16
|
+
unless samples.empty?
|
|
17
|
+
warn "threshold is beyond the max count of #{max}" if threshold > max
|
|
18
|
+
warn "threshold is below the min count of #{min}" if threshold < min
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
method_groups.select { |_, methods| methods.size >= threshold }
|
|
24
22
|
end
|
|
25
23
|
alias_method :[], :call
|
|
26
24
|
|
|
@@ -38,10 +36,14 @@ module Klee
|
|
|
38
36
|
end
|
|
39
37
|
|
|
40
38
|
def max
|
|
39
|
+
return 0 if samples.empty?
|
|
40
|
+
|
|
41
41
|
max_by { |_, v| v }.last
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def min
|
|
45
|
+
return 0 if samples.empty?
|
|
46
|
+
|
|
45
47
|
min_by { |_, v| v }.last
|
|
46
48
|
end
|
|
47
49
|
|
|
@@ -51,22 +53,22 @@ module Klee
|
|
|
51
53
|
|
|
52
54
|
private
|
|
53
55
|
|
|
56
|
+
def method_groups
|
|
57
|
+
groups = Hash.new { |h, k| h[k] = [] }
|
|
58
|
+
method_names.each do |name|
|
|
59
|
+
words(name).uniq.each { |word| groups[word.to_sym] << name.to_sym }
|
|
60
|
+
end
|
|
61
|
+
groups
|
|
62
|
+
end
|
|
63
|
+
|
|
54
64
|
def modifier_matcher
|
|
55
|
-
@modifier_matcher ||= Regexp.new
|
|
65
|
+
@modifier_matcher ||= Regexp.new(modifiers.map { Regexp.quote(it) }.join("|"))
|
|
56
66
|
end
|
|
57
67
|
|
|
58
68
|
def words(method_name)
|
|
59
|
-
method_name.to_s
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
string
|
|
63
|
-
else
|
|
64
|
-
string.gsub(modifier_matcher, "")
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
.gsub(/\s/, "")
|
|
68
|
-
.split("_")
|
|
69
|
-
.delete_if { it.empty? }
|
|
69
|
+
string = method_name.to_s
|
|
70
|
+
string = string.gsub(modifier_matcher, "") unless modifiers.empty?
|
|
71
|
+
Words.from(string)
|
|
70
72
|
end
|
|
71
73
|
end
|
|
72
74
|
end
|
data/lib/klee/file_analyzer.rb
CHANGED
|
@@ -4,6 +4,22 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
module Klee
|
|
6
6
|
class FileAnalyzer
|
|
7
|
+
ATTR_METHODS = %i[attr attr_reader attr_writer attr_accessor].freeze
|
|
8
|
+
INDEX_OPERATORS = Set[:[], :[]=].freeze
|
|
9
|
+
OPERATORS = Set.new(
|
|
10
|
+
%i[! != !~ + +@ - -@ * / % ** << >> & | ^ ~ <=> > >= < <= == === =~] +
|
|
11
|
+
INDEX_OPERATORS.to_a
|
|
12
|
+
).freeze
|
|
13
|
+
CHAIN_NOISE = %w[
|
|
14
|
+
to_s to_str to_i to_int to_a to_ary to_h to_hash to_f to_sym to_proc
|
|
15
|
+
intern itself dup clone freeze tap new
|
|
16
|
+
where includes joins order limit offset eager_load preload
|
|
17
|
+
unscope merge distinct group having lock reselect reorder
|
|
18
|
+
first last take count map select collect compact flatten uniq
|
|
19
|
+
sort sort_by reverse each
|
|
20
|
+
Hash Array Set Rails Data Logger Kernel Object
|
|
21
|
+
].to_set.freeze
|
|
22
|
+
|
|
7
23
|
attr_reader :path, :class_names, :method_names, :collaborators, :method_collaborators
|
|
8
24
|
|
|
9
25
|
def initialize(path)
|
|
@@ -19,38 +35,114 @@ module Klee
|
|
|
19
35
|
|
|
20
36
|
def parse
|
|
21
37
|
result = Prism.parse_file(@path)
|
|
22
|
-
visit(result.value, current_method: nil)
|
|
38
|
+
visit(result.value, current_method: nil, namespace: [], block_params: Set.new)
|
|
23
39
|
end
|
|
24
40
|
|
|
25
|
-
def visit(node, current_method:)
|
|
41
|
+
def visit(node, current_method:, namespace:, block_params:)
|
|
26
42
|
case node
|
|
27
43
|
when Prism::ClassNode, Prism::ModuleNode
|
|
28
|
-
|
|
44
|
+
full_name = qualified_name(node, namespace)
|
|
45
|
+
@class_names << full_name
|
|
46
|
+
nested = full_name.split("::")
|
|
47
|
+
node.child_nodes.compact.each do |child|
|
|
48
|
+
visit(child, current_method: current_method, namespace: nested, block_params: block_params)
|
|
49
|
+
end
|
|
50
|
+
return
|
|
29
51
|
when Prism::DefNode
|
|
30
52
|
@method_names << node.name.to_s
|
|
31
53
|
current_method = node.name.to_s
|
|
54
|
+
when Prism::BlockNode, Prism::LambdaNode
|
|
55
|
+
nested_params = block_params | Array(node.locals).map(&:to_s)
|
|
56
|
+
node.child_nodes.compact.each do |child|
|
|
57
|
+
visit(child, current_method: current_method, namespace: namespace, block_params: nested_params)
|
|
58
|
+
end
|
|
59
|
+
return
|
|
32
60
|
when Prism::CallNode
|
|
33
|
-
|
|
34
|
-
if
|
|
35
|
-
|
|
36
|
-
|
|
61
|
+
# Walk the receiver first so `user.account` records left-to-right.
|
|
62
|
+
if (receiver = node.receiver)
|
|
63
|
+
visit(receiver, current_method: current_method, namespace: namespace, block_params: block_params)
|
|
64
|
+
end
|
|
65
|
+
record_call(node, current_method, block_params)
|
|
66
|
+
node.child_nodes.compact.each do |child|
|
|
67
|
+
next if child.equal?(receiver)
|
|
68
|
+
|
|
69
|
+
visit(child, current_method: current_method, namespace: namespace, block_params: block_params)
|
|
37
70
|
end
|
|
71
|
+
return
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
node.child_nodes.compact.each do |child|
|
|
75
|
+
visit(child, current_method: current_method, namespace: namespace, block_params: block_params)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def record_call(node, current_method, block_params)
|
|
80
|
+
if attr_call?(node)
|
|
81
|
+
@method_names.concat(attr_names(node))
|
|
82
|
+
return
|
|
38
83
|
end
|
|
39
84
|
|
|
40
|
-
|
|
85
|
+
return if skip_operator_call?(node)
|
|
86
|
+
|
|
87
|
+
name = extract_collaborator(node)
|
|
88
|
+
return if skip_collaborator?(name, block_params)
|
|
89
|
+
|
|
90
|
+
@collaborators << name
|
|
91
|
+
@method_collaborators[current_method] << name if current_method
|
|
41
92
|
end
|
|
42
93
|
|
|
94
|
+
def skip_operator_call?(node)
|
|
95
|
+
OPERATORS.include?(node.name) && !INDEX_OPERATORS.include?(node.name)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def skip_collaborator?(name, block_params)
|
|
99
|
+
return true if name.nil? || block_params.include?(name)
|
|
100
|
+
|
|
101
|
+
key = name.to_sym
|
|
102
|
+
OPERATORS.include?(key) || CHAIN_NOISE.include?(name)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def attr_call?(node)
|
|
106
|
+
node.receiver.nil? && ATTR_METHODS.include?(node.name)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def attr_names(node)
|
|
110
|
+
args = node.arguments&.arguments || []
|
|
111
|
+
args.filter_map { |arg| attr_name_from(arg) }.map do |name|
|
|
112
|
+
(node.name == :attr_writer) ? "#{name}=" : name
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def attr_name_from(arg)
|
|
117
|
+
case arg
|
|
118
|
+
when Prism::SymbolNode, Prism::StringNode
|
|
119
|
+
arg.unescaped
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# A collaborator is the immediate receiver of a non-operator message.
|
|
43
124
|
def extract_collaborator(call_node)
|
|
44
|
-
receiver = call_node.receiver
|
|
45
|
-
case receiver
|
|
125
|
+
case (receiver = call_node.receiver)
|
|
46
126
|
when Prism::LocalVariableReadNode
|
|
47
127
|
receiver.name.to_s
|
|
48
128
|
when Prism::InstanceVariableReadNode
|
|
49
129
|
receiver.name.to_s.delete_prefix("@")
|
|
50
130
|
when Prism::CallNode
|
|
51
|
-
|
|
52
|
-
|
|
131
|
+
receiver.name.to_s
|
|
132
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
133
|
+
constant_name(receiver)
|
|
53
134
|
end
|
|
54
135
|
end
|
|
136
|
+
|
|
137
|
+
def qualified_name(node, namespace)
|
|
138
|
+
path = constant_name(node.constant_path)
|
|
139
|
+
return path if node.constant_path.is_a?(Prism::ConstantPathNode)
|
|
140
|
+
|
|
141
|
+
[*namespace, path].join("::")
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def constant_name(node)
|
|
145
|
+
node.slice.delete_prefix("::")
|
|
146
|
+
end
|
|
55
147
|
end
|
|
56
148
|
end
|
data/lib/klee/gestalt.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Klee
|
|
2
4
|
class Gestalt
|
|
3
5
|
def initialize(object, patterns:, ignored:)
|
|
@@ -33,7 +35,7 @@ module Klee
|
|
|
33
35
|
end
|
|
34
36
|
|
|
35
37
|
plot["unusual"].merge(unusual_set(threshold))
|
|
36
|
-
plot["concepts"].merge(concepts(modifiers: modifiers, threshold: concept_threshold))
|
|
38
|
+
plot["concepts"].merge(concepts(modifiers: modifiers, threshold: concept_threshold).keys)
|
|
37
39
|
|
|
38
40
|
self
|
|
39
41
|
end
|
data/lib/klee/profile.rb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Klee
|
|
4
|
+
class Profile
|
|
5
|
+
RAILS_GLOBS = %w[app/models/**/*.rb lib/**/*.rb].freeze
|
|
6
|
+
|
|
7
|
+
STOPWORDS = %w[
|
|
8
|
+
a an the and or not to for from of in on at by as
|
|
9
|
+
is are was were been being have has had do does did
|
|
10
|
+
will would could should may might must can
|
|
11
|
+
this that these those it its they them we our you your
|
|
12
|
+
if else then when
|
|
13
|
+
with without via into over per vs all any
|
|
14
|
+
id ids key keys current value values
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
def self.resolve(name, patterns:, ignore:, threshold:)
|
|
18
|
+
case name&.to_sym
|
|
19
|
+
when nil
|
|
20
|
+
{patterns: patterns, ignore: Array(ignore), threshold: threshold}
|
|
21
|
+
when :rails
|
|
22
|
+
globs = patterns.empty? ? RAILS_GLOBS : patterns
|
|
23
|
+
{
|
|
24
|
+
patterns: globs,
|
|
25
|
+
ignore: (STOPWORDS + Array(ignore).map(&:to_s)).uniq,
|
|
26
|
+
threshold: threshold
|
|
27
|
+
}
|
|
28
|
+
else
|
|
29
|
+
raise ArgumentError, "unknown profile #{name.inspect}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/klee/version.rb
CHANGED
data/lib/klee/words.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Klee
|
|
4
|
+
module Words
|
|
5
|
+
def self.from(name, ignore: [])
|
|
6
|
+
ignored = ignore.map(&:to_s)
|
|
7
|
+
tokenize(name).reject { |word| word.empty? || ignored.include?(word) }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.tokenize(name)
|
|
11
|
+
name.to_s
|
|
12
|
+
.gsub("::", "_")
|
|
13
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
14
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
15
|
+
.downcase
|
|
16
|
+
.gsub(/[?!=]/, "")
|
|
17
|
+
.split(/[_\s]+/)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/klee.rb
CHANGED
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
# require "classifier-reborn"
|
|
4
4
|
require_relative "klee/version"
|
|
5
|
+
require_relative "klee/words"
|
|
5
6
|
require_relative "klee/patterns"
|
|
6
7
|
require_relative "klee/gestalt"
|
|
7
8
|
require_relative "klee/concepts"
|
|
8
|
-
require_relative "klee/collaborators"
|
|
9
9
|
require_relative "klee/file_analyzer"
|
|
10
|
+
require_relative "klee/collaborators"
|
|
10
11
|
require_relative "klee/concept_index"
|
|
11
12
|
require_relative "klee/collaborator_index"
|
|
13
|
+
require_relative "klee/profile"
|
|
12
14
|
require_relative "klee/codebase"
|
|
13
15
|
|
|
14
16
|
module Klee
|
|
@@ -27,7 +29,7 @@ module Klee
|
|
|
27
29
|
end
|
|
28
30
|
|
|
29
31
|
def self.concepts(*method_names, modifiers: [])
|
|
30
|
-
Concepts.new(*method_names, modifiers:
|
|
32
|
+
Concepts.new(*method_names, modifiers: modifiers)
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
# def self.classifier
|
|
@@ -53,7 +55,8 @@ module Klee
|
|
|
53
55
|
Klee::Collaborators.new(const)
|
|
54
56
|
end
|
|
55
57
|
|
|
56
|
-
def self.scan(*patterns, ignore: [], threshold: 2)
|
|
57
|
-
|
|
58
|
+
def self.scan(*patterns, ignore: [], threshold: 2, profile: nil)
|
|
59
|
+
spec = Profile.resolve(profile, patterns: patterns, ignore: ignore, threshold: threshold)
|
|
60
|
+
Codebase.new(*spec[:patterns], ignore: spec[:ignore], threshold: spec[:threshold])
|
|
58
61
|
end
|
|
59
62
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: klee
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jim Gay
|
|
@@ -60,7 +60,9 @@ files:
|
|
|
60
60
|
- lib/klee/mcp/tools/find_concept_clusters.rb
|
|
61
61
|
- lib/klee/mcp/version.rb
|
|
62
62
|
- lib/klee/patterns.rb
|
|
63
|
+
- lib/klee/profile.rb
|
|
63
64
|
- lib/klee/version.rb
|
|
65
|
+
- lib/klee/words.rb
|
|
64
66
|
- sig/klee.rbs
|
|
65
67
|
homepage: https://github.com/saturnflyer/klee
|
|
66
68
|
licenses:
|
|
@@ -90,7 +92,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
90
92
|
- !ruby/object:Gem::Version
|
|
91
93
|
version: '0'
|
|
92
94
|
requirements: []
|
|
93
|
-
rubygems_version: 4.0.
|
|
95
|
+
rubygems_version: 4.0.16
|
|
94
96
|
specification_version: 4
|
|
95
97
|
summary: Evaluate the similarities and differences in your objects
|
|
96
98
|
test_files: []
|