import_graph 0.0.1
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 +7 -0
- data/lib/import_graph/dep_graph.rb +33 -0
- data/lib/import_graph/parser/autoload.rb +35 -0
- data/lib/import_graph/parser/load.rb +35 -0
- data/lib/import_graph/parser/main.rb +84 -0
- data/lib/import_graph/parser/require.rb +35 -0
- data/lib/import_graph/parser/require_relative.rb +37 -0
- data/lib/import_graph/parser/third_party.rb +47 -0
- data/lib/import_graph/path_manager.rb +24 -0
- data/lib/import_graph/scanner.rb +30 -0
- data/lib/import_graph/util.rb +15 -0
- data/lib/import_graph.rb +15 -0
- metadata +102 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 643fb90a7e318b156f633d33fbafa2a82fa5e5a0a404cf4642a62bf963f1ecc1
|
4
|
+
data.tar.gz: 0ea777046f633f6a60c71035154f2847328e11248ceb3e0a62942cc1a900a840
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d082363cd9b5ec8f4f28bad1d10a0f3aa3eec220aeccf0598cec6996855481a4376a25f219c6ea659648418efabb5fbf0c0eded700207c5418630bb3422f4609
|
7
|
+
data.tar.gz: cc8196e5d13471e03452044c0dabab5560a41a4b1497365d3177df05530044583065147f0e28be93520498429a9059a4b817ccfea0512d7b75f65c7e4f3fdc97
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImportGraph
|
4
|
+
# Source of truth for the dependency graph for the provided directory
|
5
|
+
class DepGraph
|
6
|
+
attr_accessor :graph
|
7
|
+
|
8
|
+
def initialize(graph, dir_path)
|
9
|
+
@graph = graph
|
10
|
+
@dependees = {}
|
11
|
+
@dir_path = dir_path
|
12
|
+
end
|
13
|
+
|
14
|
+
# Get all the files that this file depends on
|
15
|
+
def get_dependent_files(file_path)
|
16
|
+
return nil unless @graph.has_key? file_path
|
17
|
+
|
18
|
+
@graph[file_path]
|
19
|
+
end
|
20
|
+
|
21
|
+
# Get all the files that depend on this file
|
22
|
+
def get_dependee_files(file_path)
|
23
|
+
return @dependees[file_path] if @dependees.has_key? file_path
|
24
|
+
|
25
|
+
@dependees[file_path] = Set.new
|
26
|
+
@graph.keys.each do |key|
|
27
|
+
@dependees[file_path].add key if @graph[key].include? file_path
|
28
|
+
end
|
29
|
+
|
30
|
+
@dependees[file_path]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImportGraph
|
4
|
+
module Parser
|
5
|
+
class Autoload
|
6
|
+
include RuboCop::AST
|
7
|
+
include Util
|
8
|
+
|
9
|
+
AUTOLOAD_PATTERN = NodePattern.new <<~PATTERN
|
10
|
+
(send nil? :autoload (sym ...) (str $_))
|
11
|
+
PATTERN
|
12
|
+
|
13
|
+
def initialize(file_trees)
|
14
|
+
@file_trees = file_trees
|
15
|
+
end
|
16
|
+
|
17
|
+
def parse
|
18
|
+
matches = Set.new
|
19
|
+
@file_trees.keys.each { |file_path| matches.merge(parse_file(file_path)) }
|
20
|
+
matches
|
21
|
+
end
|
22
|
+
|
23
|
+
def parse_file(file_path)
|
24
|
+
matches = Set.new
|
25
|
+
@file_trees[file_path].each_node do |node|
|
26
|
+
match = AUTOLOAD_PATTERN.match node
|
27
|
+
next if match.nil?
|
28
|
+
|
29
|
+
matches.add(build_match_object(:autoload, file_path, match)) unless match.nil?
|
30
|
+
end
|
31
|
+
matches
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImportGraph
|
4
|
+
module Parser
|
5
|
+
class Load
|
6
|
+
include RuboCop::AST
|
7
|
+
include Util
|
8
|
+
|
9
|
+
LOAD_PATTERN = NodePattern.new <<~PATTERN
|
10
|
+
(send nil? :load (str $_))#{' '}
|
11
|
+
PATTERN
|
12
|
+
|
13
|
+
def initialize(file_trees)
|
14
|
+
@file_trees = file_trees
|
15
|
+
end
|
16
|
+
|
17
|
+
def parse
|
18
|
+
matches = Set.new
|
19
|
+
@file_trees.keys.each { |file_path| matches.merge(parse_file(file_path)) }
|
20
|
+
matches
|
21
|
+
end
|
22
|
+
|
23
|
+
def parse_file(file_path)
|
24
|
+
matches = Set.new
|
25
|
+
@file_trees[file_path].each_node do |node|
|
26
|
+
match = LOAD_PATTERN.match node
|
27
|
+
next if match.nil?
|
28
|
+
|
29
|
+
matches.add(build_match_object(:load, file_path, match)) unless match.nil?
|
30
|
+
end
|
31
|
+
matches
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImportGraph
|
4
|
+
module Parser
|
5
|
+
# Main entry point for parsing that coordinates all the parsers
|
6
|
+
class Main
|
7
|
+
include RuboCop::AST
|
8
|
+
include Util
|
9
|
+
attr_reader :file_trees
|
10
|
+
|
11
|
+
# skipcq: RB-LI1087
|
12
|
+
def initialize(dir_path, config)
|
13
|
+
@dir_path = File.expand_path(dir_path)
|
14
|
+
raise ArgumentError, "No such directory #{@dir_path}" unless Dir.exist? @dir_path
|
15
|
+
|
16
|
+
@files_list = Dir.glob(File.join(dir_path, '**', '*.rb'))
|
17
|
+
@files_list = @files_list.map { |path| File.expand_path(path) }
|
18
|
+
@path_manager = PathManager.new(@dir_path)
|
19
|
+
|
20
|
+
@parsers = [RequireRelative, Require, Load, Autoload]
|
21
|
+
@third_party = config[:third_party] || false
|
22
|
+
@file_trees = gen_syntax_trees
|
23
|
+
end
|
24
|
+
|
25
|
+
def parse
|
26
|
+
graph = {}
|
27
|
+
matches = Set.new
|
28
|
+
|
29
|
+
@parsers.each do |parser_klass|
|
30
|
+
parser = parser_klass.new(@file_trees)
|
31
|
+
local_matches = parser.parse
|
32
|
+
|
33
|
+
matches.merge(local_matches)
|
34
|
+
end
|
35
|
+
third_party_graph = {}
|
36
|
+
third_party_graph = ThirdParty.new(@dir_path).parse if @third_party == true
|
37
|
+
|
38
|
+
matches = @path_manager.resolve_paths(matches)
|
39
|
+
matches.each do |match|
|
40
|
+
add_edge(graph, match[:from], match[:to])
|
41
|
+
end
|
42
|
+
|
43
|
+
combine_graphs(graph, third_party_graph)
|
44
|
+
DepGraph.new(graph, @dir_path)
|
45
|
+
end
|
46
|
+
|
47
|
+
def add_edge(graph, from, to)
|
48
|
+
to = cleanup_absolute_path(@dir_path, to) if to.start_with? @dir_path
|
49
|
+
graph[from] = Set.new unless graph.key? from
|
50
|
+
|
51
|
+
graph[from].add to
|
52
|
+
end
|
53
|
+
|
54
|
+
def combine_graphs(main, third_party)
|
55
|
+
return if third_party.empty? || third_party.nil?
|
56
|
+
|
57
|
+
third_party.each_key do |from|
|
58
|
+
third_party[from].each do |to|
|
59
|
+
add_edge(
|
60
|
+
main,
|
61
|
+
cleanup_absolute_path(@dir_path, from),
|
62
|
+
cleanup_absolute_path(@dir_path, to)
|
63
|
+
)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def gen_syntax_trees
|
69
|
+
file_trees_list =
|
70
|
+
Parallel.map(@files_list) do |file_path|
|
71
|
+
{
|
72
|
+
path: cleanup_absolute_path(@dir_path, file_path),
|
73
|
+
tree:
|
74
|
+
ProcessedSource.new(File.read(file_path), RUBY_VERSION.to_f).ast
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
file_trees_list.each_with_object({}) do |file_pair, obj|
|
79
|
+
obj[file_pair[:path]] = file_pair[:tree]
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImportGraph
|
4
|
+
module Parser
|
5
|
+
class Require
|
6
|
+
include RuboCop::AST
|
7
|
+
include Util
|
8
|
+
|
9
|
+
REQUIRE_PATTERN = NodePattern.new <<~PATTERN
|
10
|
+
(send nil? :require (str $_))
|
11
|
+
PATTERN
|
12
|
+
|
13
|
+
def initialize(file_trees)
|
14
|
+
@file_trees = file_trees
|
15
|
+
end
|
16
|
+
|
17
|
+
def parse
|
18
|
+
matches = Set.new
|
19
|
+
@file_trees.keys.each { |file_path| matches.merge(parse_file(file_path)) }
|
20
|
+
matches
|
21
|
+
end
|
22
|
+
|
23
|
+
def parse_file(file_path)
|
24
|
+
matches = Set.new
|
25
|
+
@file_trees[file_path].each_node do |node|
|
26
|
+
match = REQUIRE_PATTERN.match node
|
27
|
+
next if match.nil?
|
28
|
+
|
29
|
+
matches.add(build_match_object(:require, file_path, match)) unless match.nil?
|
30
|
+
end
|
31
|
+
matches
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImportGraph
|
4
|
+
module Parser
|
5
|
+
# Parser that tracks all the require_relative calls in the files
|
6
|
+
class RequireRelative
|
7
|
+
include Util
|
8
|
+
include RuboCop::AST
|
9
|
+
|
10
|
+
def initialize(file_trees)
|
11
|
+
@file_trees = file_trees
|
12
|
+
end
|
13
|
+
|
14
|
+
REQUIRE_RELATIVE_PATTERN = NodePattern.new <<~PATTERN
|
15
|
+
(send nil? :require_relative (str $_))
|
16
|
+
PATTERN
|
17
|
+
|
18
|
+
def parse
|
19
|
+
matches = Set.new
|
20
|
+
@file_trees.keys.each { |file_path| matches.merge(parse_file(file_path)) }
|
21
|
+
matches
|
22
|
+
end
|
23
|
+
|
24
|
+
def parse_file(file_path)
|
25
|
+
matches = Set.new
|
26
|
+
@file_trees[file_path].each_node do |node|
|
27
|
+
match = REQUIRE_RELATIVE_PATTERN.match node
|
28
|
+
next if match.nil?
|
29
|
+
|
30
|
+
match_obj = { method: :require_relative, from: file_path, to: match }
|
31
|
+
matches.add build_match_object(:require_relative, file_path, match) unless match.nil?
|
32
|
+
end
|
33
|
+
matches
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImportGraph
|
4
|
+
module Parser
|
5
|
+
# Directories that use a third party autoloader (Zeitwerk)
|
6
|
+
# are not straightforward to parse for. In this case, `rubrowser`
|
7
|
+
# is used to scan for defined constants and if any file is found
|
8
|
+
# using that constant, we call it a dependent file.
|
9
|
+
class ThirdParty
|
10
|
+
def initialize(dir_path)
|
11
|
+
@dir_path = dir_path
|
12
|
+
end
|
13
|
+
|
14
|
+
def parse
|
15
|
+
return @dependency_graph unless @dependency_graph.nil?
|
16
|
+
|
17
|
+
obj = run_rubrowser
|
18
|
+
|
19
|
+
# Create a hash linking constant definitions with the file they were defined in
|
20
|
+
definitions = {}
|
21
|
+
obj['definitions'].each do |definition|
|
22
|
+
definitions[definition['namespace']] = definition['file']
|
23
|
+
end
|
24
|
+
|
25
|
+
# Go through the relations, if a file uses a certain constant
|
26
|
+
# add the file for that constant definition in the adjacency list
|
27
|
+
@dependency_graph = {}
|
28
|
+
obj['relations'].each do |reln|
|
29
|
+
if definitions.key? reln['resolved_namespace']
|
30
|
+
@dependency_graph[reln['file']] = [] unless @dependency_graph.key? reln['file']
|
31
|
+
@dependency_graph[reln['file']] << definitions[reln['resolved_namespace']]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
@dependency_graph
|
36
|
+
end
|
37
|
+
|
38
|
+
# runs rubrowser and returns a Ruby hash with constant definitions
|
39
|
+
# and the files they were defined in. Also a list of relations, i.e
|
40
|
+
# Which constant was used in which file
|
41
|
+
def run_rubrowser
|
42
|
+
json_output = `rubrowser -j #{@dir_path}`
|
43
|
+
JSON.parse(json_output)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImportGraph
|
4
|
+
class PathManager
|
5
|
+
def initialize(dir_path)
|
6
|
+
@dir_path = dir_path
|
7
|
+
end
|
8
|
+
|
9
|
+
def resolve_paths(matches)
|
10
|
+
# run a map on matches and modify the paths
|
11
|
+
matches.map do |match|
|
12
|
+
local = match
|
13
|
+
local[:to] += '.rb' unless local[:to].end_with? '.rb'
|
14
|
+
case local[:method]
|
15
|
+
when :require, :load, :autoload
|
16
|
+
if local[:to].start_with?('./') || local[:to].start_with?('../')
|
17
|
+
local[:to] = File.expand_path(File.join(Dir.pwd, local[:to]))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
local
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Entry point for the ImportGraph gem
|
4
|
+
module ImportGraph
|
5
|
+
# Scanner class is the starting interface though which you interact with the gem.
|
6
|
+
class Scanner
|
7
|
+
attr_reader :graph
|
8
|
+
|
9
|
+
# @param dir_path [String] Absolute path of the directory that you want scanned
|
10
|
+
# @param config [Hash] Configuration for the Scanner
|
11
|
+
def initialize(dir_path, config)
|
12
|
+
@dir_path = dir_path
|
13
|
+
@graph = nil
|
14
|
+
@config = config
|
15
|
+
end
|
16
|
+
|
17
|
+
def generate_graph
|
18
|
+
main_parser = Parser::Main.new(@dir_path, @config)
|
19
|
+
@graph = main_parser.parse
|
20
|
+
end
|
21
|
+
|
22
|
+
def get_dependents_from_file(file_path)
|
23
|
+
@graph.get_dependent_files(file_path)
|
24
|
+
end
|
25
|
+
|
26
|
+
def get_dependees_from_file(file_path)
|
27
|
+
@graph.get_dependee_files(file_path)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImportGraph
|
4
|
+
module Util
|
5
|
+
def build_match_object(method, from, to)
|
6
|
+
{ method: method, from: from, to: to }
|
7
|
+
end
|
8
|
+
|
9
|
+
def cleanup_absolute_path(dir_path, child_path)
|
10
|
+
clean_path = child_path.gsub(dir_path, '')
|
11
|
+
clean_path = clean_path[1..] if clean_path.start_with? '/'
|
12
|
+
clean_path
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/import_graph.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'zeitwerk'
|
4
|
+
require 'parallel'
|
5
|
+
require 'rubocop-ast'
|
6
|
+
require 'json'
|
7
|
+
require 'rubrowser'
|
8
|
+
require 'set'
|
9
|
+
require 'pathname'
|
10
|
+
|
11
|
+
loader = Zeitwerk::Loader.for_gem
|
12
|
+
loader.setup
|
13
|
+
|
14
|
+
module ImportGraph
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: import_graph
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Syed Faraaz Ahmad
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-02-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: parallel
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.22'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.22'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rubocop-ast
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.24'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.24'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: zeitwerk
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.6'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2.6'
|
55
|
+
description: 'Import Graph is a Gem that creates a dependency graph of all the ruby
|
56
|
+
files in a given directory.
|
57
|
+
|
58
|
+
'
|
59
|
+
email:
|
60
|
+
- faraaz@deepsource.io
|
61
|
+
executables: []
|
62
|
+
extensions: []
|
63
|
+
extra_rdoc_files: []
|
64
|
+
files:
|
65
|
+
- lib/import_graph.rb
|
66
|
+
- lib/import_graph/dep_graph.rb
|
67
|
+
- lib/import_graph/parser/autoload.rb
|
68
|
+
- lib/import_graph/parser/load.rb
|
69
|
+
- lib/import_graph/parser/main.rb
|
70
|
+
- lib/import_graph/parser/require.rb
|
71
|
+
- lib/import_graph/parser/require_relative.rb
|
72
|
+
- lib/import_graph/parser/third_party.rb
|
73
|
+
- lib/import_graph/path_manager.rb
|
74
|
+
- lib/import_graph/scanner.rb
|
75
|
+
- lib/import_graph/util.rb
|
76
|
+
homepage: https://github.com/deepsourcelabs/import-graph-ruby
|
77
|
+
licenses:
|
78
|
+
- MIT
|
79
|
+
metadata:
|
80
|
+
homepage_uri: https://github.com/deepsourcelabs/import-graph-ruby
|
81
|
+
source_code_uri: https://github.com/deepsourcelabs/import-graph-ruby
|
82
|
+
changelog_uri: https://github.com/deepsourcelabs/import-graph-ruby/blob/changelog.md
|
83
|
+
post_install_message:
|
84
|
+
rdoc_options: []
|
85
|
+
require_paths:
|
86
|
+
- lib
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: 2.6.0
|
92
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
requirements: []
|
98
|
+
rubygems_version: 3.3.7
|
99
|
+
signing_key:
|
100
|
+
specification_version: 4
|
101
|
+
summary: A gem to create a dependency graph of all the Ruby files within a directory
|
102
|
+
test_files: []
|