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 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
@@ -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: []