Class: MilkTea::ModulePathResolver

Inherits:
Object
  • Object
show all
Defined in:
lib/milk_tea/core/module_path_resolver.rb

Instance Method Summary collapse

Constructor Details

#initialize(module_roots:, platform:, package_graph: nil, source_overrides: nil, package_manifest_cache: nil) ⇒ ModulePathResolver

Returns a new instance of ModulePathResolver.



5
6
7
8
9
10
11
# File 'lib/milk_tea/core/module_path_resolver.rb', line 5

def initialize(module_roots:, platform:, package_graph: nil, source_overrides: nil, package_manifest_cache: nil)
  @module_roots = module_roots.map { |root| File.expand_path(root.to_s) }
  @platform = platform
  @package_graph = package_graph
  @source_overrides = source_overrides || {}
  @package_manifest_cache = package_manifest_cache || {}
end

Instance Method Details

#entry_module_namespace_like?(importer_path, importer_module_name) ⇒ Boolean

Returns:

  • (Boolean)


145
146
147
148
149
# File 'lib/milk_tea/core/module_path_resolver.rb', line 145

def entry_module_namespace_like?(importer_path, importer_module_name)
  return false if importer_module_name.include?(".")

  File.basename(importer_path).match?(/\Amain(?:\.(linux|windows|wasm))?\.mt\z/)
end

#import_allowed?(module_name, importer_path, candidate_path) ⇒ Boolean

Returns:

  • (Boolean)


78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/milk_tea/core/module_path_resolver.rb', line 78

def import_allowed?(module_name, importer_path, candidate_path)
  if @package_graph
    return import_allowed_by_graph?(module_name, importer_path, candidate_path)
  end

  importer_manifest = package_manifest_for_path(importer_path)
  return true unless importer_manifest

  candidate_manifest = package_manifest_for_path(candidate_path)
  return true unless candidate_manifest
  return true if candidate_manifest.manifest_path == importer_manifest.manifest_path

  dependency = importer_manifest.dependencies.find { |entry| entry.name == candidate_manifest.package_name }
  return false unless dependency

  package_namespace_match?(module_name, dependency.name)
end

#import_allowed_by_graph?(module_name, importer_path, candidate_path) ⇒ Boolean

Returns:

  • (Boolean)


96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/milk_tea/core/module_path_resolver.rb', line 96

def import_allowed_by_graph?(module_name, importer_path, candidate_path)
  importer_package = @package_graph.package_for_path(importer_path)
  return true unless importer_package

  candidate_package = @package_graph.package_for_path(candidate_path)
  return true unless candidate_package
  return true if candidate_package.manifest.manifest_path == importer_package.manifest.manifest_path

  dependency = importer_package.edges.find do |edge|
    edge.node && edge.node.manifest.package_name == candidate_package.manifest.package_name
  end
  return false unless dependency

  package_namespace_match?(module_name, dependency.dependency.name)
end

#namespace_hint_for_missing_module(module_name, importer_path:, importer_module_name:) ⇒ Object



131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/milk_tea/core/module_path_resolver.rb', line 131

def namespace_hint_for_missing_module(module_name, importer_path:, importer_module_name:)
  return nil unless importer_path && importer_module_name
  return nil unless entry_module_namespace_like?(importer_path, importer_module_name)
  return nil unless module_name.start_with?("#{importer_module_name}.")

  sibling_import = module_name.delete_prefix("#{importer_module_name}.")
  sibling_path = File.join(File.dirname(importer_path), *sibling_import.split(".")) + ".mt"
  resolved_sibling_path = ModuleLoader.resolve_source_path(sibling_path, platform: @platform)
  return nil unless source_path_available?(resolved_sibling_path)

  namespaced_path = File.join(File.dirname(importer_path), importer_module_name, *sibling_import.split(".")) + ".mt"
  "module not found; entry module '#{importer_module_name}' does not create an import namespace for sibling files. Import '#{sibling_import}' instead, or move the module to #{namespaced_path}"
end

#package_manifest_for_path(path) ⇒ Object



112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/milk_tea/core/module_path_resolver.rb', line 112

def package_manifest_for_path(path)
  return nil unless path

  package_root = ModuleRoots.package_root_for_path(path)
  return nil unless package_root

  manifest_path = File.join(package_root, "package.toml")
  return @package_manifest_cache[manifest_path] if @package_manifest_cache.key?(manifest_path)

  @package_manifest_cache[manifest_path] = PackageManifest.load(path)
rescue PackageManifestError
  @package_manifest_cache[manifest_path] = nil if manifest_path
  nil
end

#package_namespace_match?(module_name, package_name) ⇒ Boolean

Returns:

  • (Boolean)


127
128
129
# File 'lib/milk_tea/core/module_path_resolver.rb', line 127

def package_namespace_match?(module_name, package_name)
  module_name == package_name || module_name.start_with?("#{package_name}.")
end

#resolve_module_path(module_name, importer_path: nil, importer_module_name: nil) ⇒ Object

Raises:



13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/milk_tea/core/module_path_resolver.rb', line 13

def resolve_module_path(module_name, importer_path: nil, importer_module_name: nil)
  package_candidate = resolve_package_module_path(module_name, importer_path:)
  return package_candidate if package_candidate

  relative_path = File.join(*module_name.split(".")) + ".mt"
  blocked = false
  candidate = @module_roots.lazy.map { |root| ModuleLoader.resolve_source_path(File.join(root, relative_path), platform: @platform) }.find do |path|
    next false unless source_path_available?(path)

    allowed = import_allowed?(module_name, importer_path, path)
    blocked ||= !allowed
    allowed
  end
  raise ModuleLoadError.new("package dependency not declared", path: module_name) if blocked
  unless candidate
    message = namespace_hint_for_missing_module(module_name, importer_path:, importer_module_name:) || "module not found"
    raise ModuleLoadError.new(message, path: module_name)
  end

  File.expand_path(candidate)
end

#resolve_package_module_path(module_name, importer_path: nil) ⇒ Object

Raises:



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/milk_tea/core/module_path_resolver.rb', line 40

def resolve_package_module_path(module_name, importer_path: nil)
  return nil unless @package_graph && importer_path

  importer_package = @package_graph.package_for_path(importer_path)
  return nil unless importer_package

  relative_path = File.join(*module_name.split(".")) + ".mt"
  candidates = []
  if package_namespace_match?(module_name, importer_package.manifest.package_name)
    candidates << [
      importer_package.manifest.package_name,
      File.join(importer_package.manifest.source_root, relative_path),
    ]
  end

  importer_package.edges.each do |edge|
    next unless edge.node && package_namespace_match?(module_name, edge.dependency.name)

    candidates << [
      edge.dependency.name,
      File.join(edge.node.manifest.source_root, relative_path),
    ]
  end

  return nil if candidates.empty?

  best_namespace_length = candidates.map { |namespace, _path| namespace.length }.max
  matching_candidates = candidates.select { |namespace, _path| namespace.length == best_namespace_length }
  if matching_candidates.length > 1
    raise ModuleLoadError.new("ambiguous package dependency import", path: module_name)
  end

  resolved_path = ModuleLoader.resolve_source_path(matching_candidates.first.last, platform: @platform)
  raise ModuleLoadError.new("module not found", path: module_name) unless source_path_available?(resolved_path)

  File.expand_path(resolved_path)
end

#source_path_available?(path) ⇒ Boolean

Returns:

  • (Boolean)


35
36
37
38
# File 'lib/milk_tea/core/module_path_resolver.rb', line 35

def source_path_available?(path)
  resolved_path = File.expand_path(path.to_s)
  @source_overrides.key?(resolved_path) || File.file?(resolved_path)
end