vxx2 / vlib / v / util / module.v
312 lines · 300 sloc · 12.73 KB · 42fc2c0dcbfb385eb6fbbb5723a2cad01db1cf81
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 := import_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 := import_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 return mod_path_to_full_name_with_options(pref_, mod, path, false)
120}
121
122fn import_path_to_full_name(pref_ &pref.Preferences, mod string, path string) !string {
123 return mod_path_to_full_name_with_options(pref_, mod, path, true)
124}
125
126fn mod_path_to_full_name_with_options(pref_ &pref.Preferences, mod string, path string, allow_shorter_name bool) !string {
127 // TODO: explore using `pref.lookup_path` & `os.vmodules_paths()`
128 // absolute paths instead of 'vlib' & '.vmodules'
129 mut vmod_folders := ['vlib', '.vmodules', 'modules']
130 bases := pref_.lookup_path.map(os.base(it))
131 for base in bases {
132 if base !in vmod_folders {
133 vmod_folders << base
134 }
135 }
136 mut in_vmod_path := false
137 parts := path.split(os.path_separator)
138 for vmod_folder in vmod_folders {
139 if vmod_folder in parts {
140 in_vmod_path = true
141 break
142 }
143 }
144 // Anchor the module-name boundary to the v.mod that contains the
145 // current compilation (`pref_.path`). Without this, a nested v.mod
146 // inside the project (e.g. a vendored sub-project at `dep/v.mod`)
147 // would shrink the qualified name: files at `dep/mymod` would become
148 // `mymod` instead of `dep.mymod`, breaking `import dep.mymod` from
149 // the outer project. See issue #27138.
150 pref_project_root := if in_vmod_path { '' } else { project_root_vmod_folder(pref_) }
151 path_parts := path.split(os.path_separator)
152 mod_path := mod.replace('.', os.path_separator)
153 // go back through each parent in path_parts and join with `mod_path` to see the dir exists
154 for i := path_parts.len - 1; i > 0; i-- {
155 try_path := os.join_path_single(path_parts[0..i].join(os.path_separator), mod_path)
156 // found module path
157 if os.is_dir(try_path) {
158 // we know we are in one of the `vmod_folders`
159 if in_vmod_path {
160 // so we can work our way backwards until we reach a vmod folder
161 for j := i; j >= 0; j-- {
162 path_part := path_parts[j]
163 // we reached a vmod folder
164 if path_part in vmod_folders {
165 mod_full_name := normalize_base_url_mod_name(try_path.split(os.path_separator)[
166 j + 1..].join('.'), try_path)
167 return mod_full_name
168 }
169 }
170 // not in one of the `vmod_folders` so work backwards through each parent
171 // looking for for a `v.mod` file and break at the first path without it
172 } else {
173 if pref_project_root != '' {
174 real_try_path := os.real_path(try_path)
175 prefix := pref_project_root + os.path_separator
176 if real_try_path.starts_with(prefix) {
177 relative_parts := real_try_path.all_after(prefix).split(os.path_separator)
178 mod_full_name := normalize_base_url_mod_name(relative_parts.join('.'),
179 try_path)
180 if !allow_shorter_name && mod_full_name.len < mod.len {
181 return mod
182 }
183 if !module_name_has_empty_part(mod_full_name) {
184 return mod_full_name
185 }
186 }
187 }
188 mut try_path_parts := try_path.split(os.path_separator)
189 // last index in try_path_parts that contains a `v.mod`
190 mut last_v_mod := -1
191 for j := try_path_parts.len; j > 0; j-- {
192 parent := try_path_parts[0..j].join(os.path_separator)
193 if ls := os.ls(parent) {
194 // currently CI clones some modules into the v repo to test, the condition
195 // after `'v.mod' in ls` can be removed once a proper solution is added
196 if 'v.mod' in ls
197 && (try_path_parts.len > i && try_path_parts[i] != 'v' && 'vlib' !in ls) {
198 last_v_mod = j
199 break
200 }
201 continue
202 }
203 break
204 }
205 if last_v_mod > -1 {
206 mod_full_name := normalize_base_url_mod_name(try_path_parts[last_v_mod..].join('.'),
207 try_path)
208 if !allow_shorter_name && mod_full_name.len < mod.len {
209 return mod
210 }
211 if !module_name_has_empty_part(mod_full_name) {
212 return mod_full_name
213 }
214 }
215 }
216 }
217 }
218 if os.is_abs_path(path) && os.is_dir(path) { // && path.contains(mod )
219 abs_pref_path := if os.is_abs_path(pref_.path) {
220 pref_.path
221 } else {
222 os.join_path_single(os.getwd(), pref_.path)
223 }
224 rel_mod_path := path.replace(abs_pref_path.all_before_last(os.path_separator) +
225 os.path_separator, '')
226 if rel_mod_path != path {
227 return normalize_base_url_mod_name(rel_mod_path.replace(os.path_separator, '.'), path)
228 }
229 }
230 return error('module not found')
231}
232
233fn module_name_has_empty_part(name string) bool {
234 if name == '' {
235 return true
236 }
237 for part in name.split('.') {
238 if part == '' {
239 return true
240 }
241 }
242 return false
243}
244
245// project_root_vmod_folder returns the absolute folder of the closest
246// enclosing v.mod for the current compilation (`pref_.path`). Module-name
247// qualification uses this as the boundary so a nested v.mod inside the
248// project does not silently rename its sub-modules.
249fn project_root_vmod_folder(pref_ &pref.Preferences) string {
250 if pref_.path == '' {
251 return ''
252 }
253 abs_pref_path := if os.is_abs_path(pref_.path) {
254 pref_.path
255 } else {
256 os.join_path_single(os.getwd(), pref_.path)
257 }
258 start := if os.is_dir(abs_pref_path) { abs_pref_path } else { os.dir(abs_pref_path) }
259 if start == '' {
260 return ''
261 }
262 mut cfolder := os.real_path(start)
263 for {
264 if os.is_file(os.join_path(cfolder, 'v.mod')) {
265 return cfolder
266 }
267 parent := os.dir(cfolder)
268 if parent == cfolder || parent == '' {
269 return ''
270 }
271 cfolder = parent
272 }
273 return ''
274}
275
276// normalize_base_url_mod_name strips the `base_url` prefix from `mod_full_name`
277// when the module lives in a folder configured via v.mod's `base_url`. Without
278// this, a module rooted at `<pkg>/source/feature` would be named `pkg.source.feature`
279// instead of `pkg.feature`. The implicit `src/` fallback is intentionally gone.
280fn normalize_base_url_mod_name(mod_full_name string, path string) string {
281 real_path := os.real_path(path)
282 mut mcache := vmod.get_cache()
283 vmod_file_location := mcache.get_by_folder(real_path)
284 if vmod_file_location.vmod_file == '' {
285 return mod_full_name
286 }
287 vmod_prefix := vmod_file_location.vmod_folder + os.path_separator
288 if !real_path.starts_with(vmod_prefix) {
289 return mod_full_name
290 }
291 manifest := vmod.from_file(vmod_file_location.vmod_file) or { return mod_full_name }
292 if manifest.base_url == '' {
293 return mod_full_name
294 }
295 base_parts := os.norm_path(manifest.base_url).split(os.path_separator).filter(it.len > 0
296 && it != '.')
297 if base_parts.len == 0 {
298 return mod_full_name
299 }
300 rel_path := real_path.all_after(vmod_prefix)
301 rel_parts := rel_path.split(os.path_separator)
302 if rel_parts.len < base_parts.len || rel_parts[..base_parts.len] != base_parts {
303 return mod_full_name
304 }
305 full_parts := mod_full_name.split('.')
306 if rel_parts.len > full_parts.len {
307 return mod_full_name
308 }
309 mut normalized_parts := full_parts[..full_parts.len - rel_parts.len].clone()
310 normalized_parts << rel_parts[base_parts.len..]
311 return normalized_parts.join('.')
312}
313