| 1 | module 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. |
| 5 | import os |
| 6 | import v.pref |
| 7 | import v.vmod |
| 8 | |
| 9 | @[if trace_util_qualify ?] |
| 10 | fn 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. |
| 23 | pub 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. |
| 66 | pub 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) |
| 118 | fn 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. |
| 202 | fn 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 | |