import_graph 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []