v2 / vlib / v / util / module.v
234 lines · 226 sloc · 10.29 KB · 823f10d197d94ee34be10ca037fef2ceef38274d
Raw
1module util
2
3// 2022-01-30 TODO: this whole file should not exist :-|. It should just use the existing `v.vmod` instead,
4// 2022-01-30 that already does handle v.mod lookup properly, stopping at .git folders, supporting `.v.mod.stop` etc.
5import os
6import v.pref
7import v.vmod
8
9@[if trace_util_qualify ?]
10fn trace_qualify(callfn string, mod string, file_path string, kind_res string, result string, detail string) {
11 eprintln('> ${callfn:15}: ${mod:-18} | file_path: ${file_path:-71} | => ${kind_res:14}: ${result:-18} ; ${detail}')
12}
13
14// 2022-01-30 qualify_import - used by V's parser, to find the full module name of import statements
15// 2022-01-30 i.e. when parsing `import automaton` inside a .v file in examples/game_of_life/life_gg.v
16// 2022-01-30 it returns just 'automaton'
17// 2022-01-30 TODO: this seems to always just return `mod` itself, for modules inside the V main folder.
18// 2022-01-30 It does also return `mod` itself, for stuff installed in ~/.vmodules like `vls` but for
19// 2022-01-30 other reasons (see res 2 below).
20
21// qualify_import is used by V's parser, to find the full module name of import statements.
22// Do not use it.
23pub fn qualify_import(pref_ &pref.Preferences, mod string, file_path string) string {
24 // comments are from workdir: /v/vls
25 mut mod_paths := pref_.lookup_path.clone()
26 mod_paths << os.vmodules_paths()
27 mod_path := mod.replace('.', os.path_separator)
28 for search_path in mod_paths {
29 try_path := os.join_path_single(search_path, mod_path)
30 if os.is_dir(try_path) {
31 if m1 := mod_path_to_full_name(pref_, mod, try_path) {
32 trace_qualify(@FN, mod, file_path, 'import_res 1', m1, try_path)
33 // > qualify_import: term | file_path: /v/vls/server/diagnostics.v | => import_res 1: term ; /v/cleanv/vlib/term
34 return m1
35 }
36 }
37 }
38 // Use absolute file_path so mod_path_to_full_name can walk up to find v.mod
39 abs_file_path := if os.is_abs_path(file_path) {
40 file_path
41 } else {
42 os.join_path_single(os.getwd(), file_path)
43 }
44 if m1 := mod_path_to_full_name(pref_, mod, abs_file_path) {
45 trace_qualify(@FN, mod, file_path, 'import_res 2', m1, abs_file_path)
46 // > qualify_module: analyzer | file_path: /v/vls/analyzer/store.v | => module_res 2: analyzer ; clean_file_path - getwd == mod
47 // > qualify_import: analyzer.depgraph | file_path: /v/vls/analyzer/store.v | => import_res 2: analyzer.depgraph ; /v/vls/analyzer/store.v
48 // > qualify_import: tree_sitter | file_path: /v/vls/analyzer/store.v | => import_res 2: tree_sitter ; /v/vls/analyzer/store.v
49 // > qualify_import: tree_sitter_v | file_path: /v/vls/analyzer/store.v | => import_res 1: tree_sitter_v ; ~/.vmodules/tree_sitter_v
50 // > qualify_import: jsonrpc | file_path: /v/vls/server/features.v | => import_res 2: jsonrpc ; /v/vls/server/features.v
51 return m1
52 }
53 trace_qualify(@FN, mod, file_path, 'import_res 3', mod, '---, mod_path: ${mod_path}')
54 // > qualify_import: server | file_path: cmd/vls/host.v | => import_res 3: server ; ---
55 // > qualify_import: cli | file_path: cmd/vls/main.v | => import_res 1: cli ; /v/cleanv/vlib/cli
56 // > qualify_import: server | file_path: cmd/vls/main.v | => import_res 3: server ; ---
57 // > qualify_import: os | file_path: cmd/vls/main.v | => import_res 1: os ; /v/cleanv/vlib/os
58 return mod
59}
60
61// 2022-01-30 qualify_module - used by V's parser to find the full module name
62// 2022-01-30 i.e. when parsing `module textscanner`, inside vlib/strings/textscanner/textscanner.v
63// 2022-01-30 it will return `strings.textscanner`
64
65// qualify_module - used by V's parser to find the full module name. Do not use it.
66pub fn qualify_module(pref_ &pref.Preferences, mod string, file_path string) string {
67 if mod == 'main' {
68 trace_qualify(@FN, mod, file_path, 'module_res 1', mod, 'main')
69 return mod
70 }
71 clean_file_path := file_path.all_before_last(os.path_separator)
72 // Use absolute path so mod_path_to_full_name can walk up to find v.mod
73 abs_clean_file_path := if os.is_abs_path(clean_file_path) {
74 clean_file_path
75 } else {
76 os.join_path_single(os.getwd(), clean_file_path)
77 }
78 // relative module (relative to working directory)
79 // TODO: find most stable solution & test with -usecache
80 //
81 // TODO: 2022-01-30: Using os.getwd() here does not seem right *at all* imho.
82 // TODO: 2022-01-30: That makes lookup dependent on fragile environment factors.
83 // TODO: 2022-01-30: The lookup should be relative to the folder, in which the current file is,
84 // TODO: 2022-01-30: *NOT* to the working folder of the compiler, which can change easily.
85 if clean_file_path.replace(os.getwd() + os.path_separator, '') == mod {
86 if m1 := mod_path_to_full_name(pref_, mod, abs_clean_file_path) {
87 if m1 != mod {
88 trace_qualify(@FN, mod, file_path, 'module_res 2', m1,
89 'clean_file_path - getwd == mod, m1 == f(${abs_clean_file_path})')
90 return m1
91 }
92 }
93 trace_qualify(@FN, mod, file_path, 'module_res 2', mod,
94 'clean_file_path - getwd == mod, clean_file_path: ${clean_file_path}')
95 return mod
96 }
97 if m1 := mod_path_to_full_name(pref_, mod, abs_clean_file_path) {
98 trace_qualify(@FN, mod, file_path, 'module_res 3', m1, 'm1 == f(${abs_clean_file_path})')
99 return m1
100 }
101 trace_qualify(@FN, mod, file_path, 'module_res 4', mod,
102 '---, clean_file_path: ${clean_file_path}')
103 return mod
104}
105
106// TODO:
107// * properly define module location / v.mod rules
108// * if possible split this function in two, one which gets the
109// parent module path and another which turns it into the full name
110// * create shared logic between these fns and builder.find_module_path
111// 2022-01-30 TODO: the reliance on os.path_separator here, is also a potential problem.
112// 2022-01-30 On windows that leads to:
113// 2022-01-30 `v path/subfolder/` behaving very differently than `v path\subfolder\`
114// 2022-01-30 (see daa5be4, that skips checking `vlib/v/checker/tests/modules/deprecated_module`
115// 2022-01-30 just on windows, because while `vlib\v\checker\tests\modules\deprecated_module` works,
116// 2022-01-30 it leads to path differences, and the / version on windows triggers a module lookip bug,
117// 2022-01-30 leading to completely different errors)
118fn mod_path_to_full_name(pref_ &pref.Preferences, mod string, path string) !string {
119 // TODO: explore using `pref.lookup_path` & `os.vmodules_paths()`
120 // absolute paths instead of 'vlib' & '.vmodules'
121 mut vmod_folders := ['vlib', '.vmodules', 'modules']
122 bases := pref_.lookup_path.map(os.base(it))
123 for base in bases {
124 if base !in vmod_folders {
125 vmod_folders << base
126 }
127 }
128 mut in_vmod_path := false
129 parts := path.split(os.path_separator)
130 for vmod_folder in vmod_folders {
131 if vmod_folder in parts {
132 in_vmod_path = true
133 break
134 }
135 }
136 path_parts := path.split(os.path_separator)
137 mod_path := mod.replace('.', os.path_separator)
138 // go back through each parent in path_parts and join with `mod_path` to see the dir exists
139 for i := path_parts.len - 1; i > 0; i-- {
140 try_path := os.join_path_single(path_parts[0..i].join(os.path_separator), mod_path)
141 // found module path
142 if os.is_dir(try_path) {
143 // we know we are in one of the `vmod_folders`
144 if in_vmod_path {
145 // so we can work our way backwards until we reach a vmod folder
146 for j := i; j >= 0; j-- {
147 path_part := path_parts[j]
148 // we reached a vmod folder
149 if path_part in vmod_folders {
150 mod_full_name := normalize_base_url_mod_name(try_path.split(os.path_separator)[
151 j + 1..].join('.'), try_path)
152 return mod_full_name
153 }
154 }
155 // not in one of the `vmod_folders` so work backwards through each parent
156 // looking for for a `v.mod` file and break at the first path without it
157 } else {
158 mut try_path_parts := try_path.split(os.path_separator)
159 // last index in try_path_parts that contains a `v.mod`
160 mut last_v_mod := -1
161 for j := try_path_parts.len; j > 0; j-- {
162 parent := try_path_parts[0..j].join(os.path_separator)
163 if ls := os.ls(parent) {
164 // currently CI clones some modules into the v repo to test, the condition
165 // after `'v.mod' in ls` can be removed once a proper solution is added
166 if 'v.mod' in ls
167 && (try_path_parts.len > i && try_path_parts[i] != 'v' && 'vlib' !in ls) {
168 last_v_mod = j
169 break
170 }
171 continue
172 }
173 break
174 }
175 if last_v_mod > -1 {
176 mod_full_name := normalize_base_url_mod_name(try_path_parts[last_v_mod..].join('.'),
177 try_path)
178 return if mod_full_name.len < mod.len { mod } else { mod_full_name }
179 }
180 }
181 }
182 }
183 if os.is_abs_path(path) && os.is_dir(path) { // && path.contains(mod )
184 abs_pref_path := if os.is_abs_path(pref_.path) {
185 pref_.path
186 } else {
187 os.join_path_single(os.getwd(), pref_.path)
188 }
189 rel_mod_path := path.replace(abs_pref_path.all_before_last(os.path_separator) +
190 os.path_separator, '')
191 if rel_mod_path != path {
192 return normalize_base_url_mod_name(rel_mod_path.replace(os.path_separator, '.'), path)
193 }
194 }
195 return error('module not found')
196}
197
198// normalize_base_url_mod_name strips the `base_url` prefix from `mod_full_name`
199// when the module lives in a folder configured via v.mod's `base_url`. Without
200// this, a module rooted at `<pkg>/source/feature` would be named `pkg.source.feature`
201// instead of `pkg.feature`. The implicit `src/` fallback is intentionally gone.
202fn normalize_base_url_mod_name(mod_full_name string, path string) string {
203 real_path := os.real_path(path)
204 mut mcache := vmod.get_cache()
205 vmod_file_location := mcache.get_by_folder(real_path)
206 if vmod_file_location.vmod_file == '' {
207 return mod_full_name
208 }
209 vmod_prefix := vmod_file_location.vmod_folder + os.path_separator
210 if !real_path.starts_with(vmod_prefix) {
211 return mod_full_name
212 }
213 manifest := vmod.from_file(vmod_file_location.vmod_file) or { return mod_full_name }
214 if manifest.base_url == '' {
215 return mod_full_name
216 }
217 base_parts := os.norm_path(manifest.base_url).split(os.path_separator).filter(it.len > 0
218 && it != '.')
219 if base_parts.len == 0 {
220 return mod_full_name
221 }
222 rel_path := real_path.all_after(vmod_prefix)
223 rel_parts := rel_path.split(os.path_separator)
224 if rel_parts.len < base_parts.len || rel_parts[..base_parts.len] != base_parts {
225 return mod_full_name
226 }
227 full_parts := mod_full_name.split('.')
228 if rel_parts.len > full_parts.len {
229 return mod_full_name
230 }
231 mut normalized_parts := full_parts[..full_parts.len - rel_parts.len].clone()
232 normalized_parts << rel_parts[base_parts.len..]
233 return normalized_parts.join('.')
234}
235