⭐️ A friendly language for building type-safe, scalable systems!

Warn for duplicate imports

Closes https://github.com/gleam-lang/gleam/issues/4665

+4
CHANGELOG.md
··· 4 4 5 5 ### Compiler 6 6 7 + - A warning is now emitted when the same module is imported is imported 8 + multiple times into the same module with different aliases. 9 + ([Louis Pilfold](https://github.com/lpil)) 10 + 7 11 ### Build tool 8 12 9 13 ### Language server
+41 -26
compiler-core/src/analyse/imports.rs
··· 5 5 build::Origin, 6 6 reference::{EntityKind, ReferenceKind}, 7 7 type_::{ 8 - Environment, Error, ModuleInterface, Problems, ValueConstructorVariant, 8 + Environment, Error, ModuleInterface, Problems, ValueConstructorVariant, Warning, 9 9 error::InvalidImportKind, 10 10 }, 11 11 }; ··· 271 271 import: &UntypedImport, 272 272 import_info: &'context ModuleInterface, 273 273 ) -> Result<(), Error> { 274 - if let Some(used_name) = import.used_name() { 275 - self.check_not_a_duplicate_import(&used_name, import.location)?; 274 + let Some(used_name) = import.used_name() else { 275 + return Ok(()); 276 + }; 277 + 278 + self.check_not_a_duplicate_import(&used_name, import.location)?; 276 279 277 - if let Some(alias_location) = import.alias_location() { 278 - self.environment.references.register_aliased_module( 279 - used_name.clone(), 280 - import.module.clone(), 281 - alias_location, 282 - import.location, 283 - ); 284 - } else { 285 - self.environment.references.register_module( 286 - used_name.clone(), 287 - import.module.clone(), 288 - import.location, 289 - ); 290 - } 280 + if let Some(alias_location) = import.alias_location() { 281 + self.environment.references.register_aliased_module( 282 + used_name.clone(), 283 + import.module.clone(), 284 + alias_location, 285 + import.location, 286 + ); 287 + } else { 288 + self.environment.references.register_module( 289 + used_name.clone(), 290 + import.module.clone(), 291 + import.location, 292 + ); 293 + } 291 294 292 - // Insert imported module into scope 293 - let _ = self 294 - .environment 295 - .imported_modules 296 - .insert(used_name.clone(), (import.location, import_info)); 295 + // Insert imported module into scope 296 + let _ = self 297 + .environment 298 + .imported_modules 299 + .insert(used_name.clone(), (import.location, import_info)); 297 300 298 - self.environment 299 - .names 300 - .imported_module(import.module.clone(), used_name); 301 - }; 301 + // Register this module as being imported 302 + // 303 + // Emit a warning if the module had already been imported. 304 + // This isn't an error so long as the modules have different local aliases. In Gleam v2 305 + // this will likely become an error. 306 + if let Some(previous) = self.environment.names.imported_module( 307 + import.module.clone(), 308 + used_name, 309 + import.location, 310 + ) { 311 + self.problems.warning(Warning::ModuleImportedTwice { 312 + name: import.module.clone(), 313 + first: previous, 314 + second: import.location, 315 + }); 316 + } 302 317 303 318 Ok(()) 304 319 }
+6 -1
compiler-core/src/exhaustiveness/printer.rs
··· 193 193 use std::{collections::HashMap, sync::Arc}; 194 194 195 195 use crate::{ 196 + ast::SrcSpan, 196 197 exhaustiveness::{ 197 198 Variable, 198 199 missing_patterns::{Term, VariantField}, ··· 321 322 fn test_module_alias() { 322 323 let mut names = Names::new(); 323 324 324 - names.imported_module("mod".into(), "shapes".into()); 325 + assert!( 326 + names 327 + .imported_module("mod".into(), "shapes".into(), SrcSpan::new(50, 60)) 328 + .is_none() 329 + ); 325 330 326 331 let printer = Printer::new(&names); 327 332
+12 -1
compiler-core/src/type_/error.rs
··· 983 983 truncation: BitArraySegmentTruncation, 984 984 location: SrcSpan, 985 985 }, 986 + 987 + /// In Gleam v1 it is possible to import one module twice using different aliases. 988 + /// This is deprecated, and likely would be removed in a Gleam v2. 989 + ModuleImportedTwice { 990 + name: EcoString, 991 + first: SrcSpan, 992 + second: SrcSpan, 993 + }, 986 994 } 987 995 988 996 #[derive(Debug, Eq, Copy, PartialEq, Clone, serde::Serialize, serde::Deserialize)] ··· 1207 1215 | Warning::FeatureRequiresHigherGleamVersion { location, .. } 1208 1216 | Warning::JavaScriptIntUnsafe { location, .. } 1209 1217 | Warning::AssertLiteralValue { location, .. } 1210 - | Warning::BitArraySegmentTruncatedValue { location, .. } => *location, 1218 + | Warning::BitArraySegmentTruncatedValue { location, .. } 1219 + | Warning::ModuleImportedTwice { 1220 + second: location, .. 1221 + } => *location, 1211 1222 } 1212 1223 } 1213 1224
+39 -11
compiler-core/src/type_/printer.rs
··· 3 3 use im::HashMap; 4 4 use std::{collections::HashSet, sync::Arc}; 5 5 6 - use crate::type_::{Type, TypeVar}; 6 + use crate::{ 7 + ast::SrcSpan, 8 + type_::{Type, TypeVar}, 9 + }; 7 10 8 11 /// This class keeps track of what names are used for modules in the current 9 12 /// scope, so they can be printed in errors, etc. ··· 68 71 /// - key: "mod1" 69 72 /// - value: "mod1" 70 73 /// 71 - imported_modules: HashMap<EcoString, EcoString>, 74 + imported_modules: HashMap<EcoString, (EcoString, SrcSpan)>, 72 75 73 76 /// Generic type parameters that have been annotated in the current 74 77 /// function. ··· 176 179 } 177 180 178 181 /// Record an imported module in this module. 179 - pub fn imported_module(&mut self, module_name: EcoString, module_alias: EcoString) { 180 - _ = self.imported_modules.insert(module_name, module_alias) 182 + /// 183 + /// Returns the location of the previous time this module was imported, if there was one. 184 + pub fn imported_module( 185 + &mut self, 186 + module_name: EcoString, 187 + module_alias: EcoString, 188 + location: SrcSpan, 189 + ) -> Option<SrcSpan> { 190 + self.imported_modules 191 + .insert(module_name, (module_alias, location)) 192 + .map(|(_, location)| location) 181 193 } 182 194 183 195 /// Get the name and optional module qualifier for a named type. ··· 188 200 print_mode: PrintMode, 189 201 ) -> NameContextInformation<'a> { 190 202 if print_mode == PrintMode::ExpandAliases { 191 - if let Some(module) = self.imported_modules.get(module) { 203 + if let Some((module, _)) = self.imported_modules.get(module) { 192 204 return NameContextInformation::Qualified(module, name.as_str()); 193 205 }; 194 206 ··· 204 216 } 205 217 206 218 // This type is from a module that has been imported 207 - if let Some(module) = self.imported_modules.get(module) { 219 + if let Some((module, _)) = self.imported_modules.get(module) { 208 220 return NameContextInformation::Qualified(module, name.as_str()); 209 221 }; 210 222 ··· 238 250 } 239 251 240 252 // This value is from a module that has been imported 241 - if let Some(module) = self.imported_modules.get(module) { 253 + if let Some((module, _)) = self.imported_modules.get(module) { 242 254 return NameContextInformation::Qualified(module, name.as_str()); 243 255 }; 244 256 ··· 326 338 327 339 pub fn print_module(&self, module: &str) -> EcoString { 328 340 match self.names.imported_modules.get(module) { 329 - Some(module) => module.clone(), 341 + Some((module, _)) => module.clone(), 330 342 _ => module.split("/").last().unwrap_or(module).into(), 331 343 } 332 344 } ··· 617 629 #[test] 618 630 fn test_module_alias() { 619 631 let mut names = Names::new(); 620 - names.imported_module("mod1".into(), "animals".into()); 632 + 633 + assert!( 634 + names 635 + .imported_module("mod1".into(), "animals".into(), SrcSpan::new(50, 63)) 636 + .is_none() 637 + ); 638 + 621 639 let mut printer = Printer::new(&names); 622 640 623 641 let type_ = Type::Named { ··· 700 718 fn test_unqualified_import_and_module_alias() { 701 719 let mut names = Names::new(); 702 720 703 - names.imported_module("mod1".into(), "animals".into()); 721 + assert!( 722 + names 723 + .imported_module("mod1".into(), "animals".into(), SrcSpan::new(76, 93)) 724 + .is_none() 725 + ); 704 726 705 727 let _ = names 706 728 .local_types ··· 723 745 #[test] 724 746 fn test_module_imports() { 725 747 let mut names = Names::new(); 726 - names.imported_module("mod".into(), "animals".into()); 748 + 749 + assert!( 750 + names 751 + .imported_module("mod".into(), "animals".into(), SrcSpan::new(76, 93)) 752 + .is_none() 753 + ); 754 + 727 755 let _ = names 728 756 .local_types 729 757 .insert(("mod2".into(), "Cat".into()), "Cat".into());
+29
compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__import_module_twice.snap
··· 1 + --- 2 + source: compiler-core/src/type_/tests/warnings.rs 3 + assertion_line: 3918 4 + expression: "import gleam/wibble as a\nimport gleam/wibble as b\n\npub fn main() {\n a.wobble() + b.wobble()\n}\n" 5 + snapshot_kind: text 6 + --- 7 + ----- SOURCE CODE 8 + -- gleam/wibble.gleam 9 + pub fn wobble() { 1 } 10 + 11 + -- main.gleam 12 + import gleam/wibble as a 13 + import gleam/wibble as b 14 + 15 + pub fn main() { 16 + a.wobble() + b.wobble() 17 + } 18 + 19 + 20 + ----- WARNING 21 + warning: Duplicate import 22 + ┌─ /src/warning/wrn.gleam:1:1 23 + 24 + 1 │ import gleam/wibble as a 25 + │ ^^^^^^^^^^^^^^^^^^^^^^^^ 26 + 2 │ import gleam/wibble as b 27 + │ ^^^^^^^^^^^^^^^^^^^^^^^^ 28 + 29 + The gleam/wibble module has been imported twice.
+14
compiler-core/src/type_/tests/warnings.rs
··· 3912 3912 "# 3913 3913 ); 3914 3914 } 3915 + 3916 + #[test] 3917 + fn import_module_twice() { 3918 + assert_warning!( 3919 + ("gleam/wibble", "pub fn wobble() { 1 }"), 3920 + "import gleam/wibble as a 3921 + import gleam/wibble as b 3922 + 3923 + pub fn main() { 3924 + a.wobble() + b.wobble() 3925 + } 3926 + " 3927 + ); 3928 + }
+27 -1
compiler-core/src/warning.rs
··· 1 1 use crate::{ 2 2 ast::{BitArraySegmentTruncation, SrcSpan, TodoKind}, 3 3 build::Target, 4 - diagnostic::{self, Diagnostic, Location}, 4 + diagnostic::{self, Diagnostic, ExtraLabel, Location}, 5 5 error::wrap, 6 6 type_::{ 7 7 self, ··· 1274 1274 span: *location, 1275 1275 }, 1276 1276 extra_labels: Vec::new(), 1277 + }), 1278 + }, 1279 + 1280 + type_::Warning::ModuleImportedTwice { 1281 + name, 1282 + first, 1283 + second, 1284 + } => Diagnostic { 1285 + title: "Duplicate import".into(), 1286 + text: format!("The {name} module has been imported twice."), 1287 + hint: None, 1288 + level: diagnostic::Level::Warning, 1289 + location: Some(Location { 1290 + src: src.clone(), 1291 + path: path.to_path_buf(), 1292 + label: diagnostic::Label { 1293 + text: None, 1294 + span: *first, 1295 + }, 1296 + extra_labels: vec![ExtraLabel { 1297 + src_info: None, 1298 + label: diagnostic::Label { 1299 + text: None, 1300 + span: *second, 1301 + }, 1302 + }], 1277 1303 }), 1278 1304 }, 1279 1305 },
+6
test-package-compiler/cases/imported_external_fns/src/three.gleam
··· 1 + @external(erlang, "thing", "new") 2 + pub fn thing() -> Nil 3 + 4 + // https://github.com/gleam-lang/gleam/issues/4507 5 + @external(erlang, "the.thing", "make.new") 6 + pub fn escaped_thing() -> Nil
+6 -6
test-package-compiler/cases/imported_external_fns/src/two.gleam
··· 1 1 import one.{ 2 2 escaped_thing, escaped_thing as xescaped_thing, thing, thing as xthing, 3 3 } 4 - import one as xone 4 + import three as aliased 5 5 6 6 // Assigning a function to constants 7 7 8 8 const const_qualified = one.thing 9 9 10 - const const_qualified_aliased = xone.thing 10 + const const_qualified_aliased = aliased.thing 11 11 12 12 const const_unqualified = thing 13 13 ··· 15 15 16 16 const escaped_const_qualified = one.escaped_thing 17 17 18 - const escaped_const_qualified_aliased = xone.escaped_thing 18 + const escaped_const_qualified_aliased = aliased.escaped_thing 19 19 20 20 const escaped_const_unqualified = escaped_thing 21 21 ··· 47 47 } 48 48 49 49 pub fn fn_reference_qualified_aliased() { 50 - xone.thing 50 + aliased.thing 51 51 } 52 52 53 53 pub fn fn_reference_unqualified() { ··· 65 65 } 66 66 67 67 pub fn fn_call_qualified_aliased() { 68 - xone.thing() 68 + aliased.thing() 69 69 } 70 70 71 71 pub fn fn_call_unqualified() { ··· 83 83 } 84 84 85 85 pub fn argument_reference_qualified_aliased() { 86 - x(xone.escaped_thing) 86 + x(aliased.escaped_thing) 87 87 } 88 88 89 89 pub fn argument_reference_unqualified() {
+11
test-package-compiler/cases/imported_record_constructors/src/one/two.gleam
··· 1 + pub type A { 2 + A 3 + } 4 + 5 + pub type B { 6 + B(A, A) 7 + } 8 + 9 + pub type User { 10 + User(name: String, score: Int) 11 + }
+5 -5
test-package-compiler/cases/imported_record_constructors/src/two.gleam
··· 1 1 import one/one.{A, A as C, B, B as D, User, User as XUser} 2 - import one/one as xone 2 + import one/two as aliased 3 3 4 4 /// For these statements we use the records in a qualified fashion 5 5 pub const qualified_const_a = one.A ··· 14 14 qualified_const_b 15 15 } 16 16 17 - pub const qualified_aliased_const_a = xone.A 17 + pub const qualified_aliased_const_a = aliased.A 18 18 19 19 pub fn qualified_aliased_fn_a() { 20 20 qualified_aliased_const_a 21 21 } 22 22 23 - pub const qualified_aliased_const_b = xone.B(xone.A, xone.A) 23 + pub const qualified_aliased_const_b = aliased.B(aliased.A, aliased.A) 24 24 25 25 pub fn qualified_aliased_fn_b() { 26 26 qualified_aliased_const_b ··· 68 68 } 69 69 70 70 pub fn destructure_qualified_aliased(user) { 71 - let xone.User(name: name, score: score) = user 71 + let aliased.User(name: name, score: score) = user 72 72 #(name, score) 73 73 } 74 74 ··· 88 88 } 89 89 90 90 pub fn update_qualified_aliased(user) { 91 - xone.User(..user, name: "wibble") 91 + aliased.User(..user, name: "wibble") 92 92 } 93 93 94 94 pub fn update_unqualified(user) {
+25 -1
test-package-compiler/src/snapshots/test_package_compiler__generated_tests__imported_external_fns.snap
··· 27 27 'the.thing':'make.new'(). 28 28 29 29 30 + //// /out/lib/the_package/_gleam_artefacts/three.cache 31 + <.cache binary> 32 + 33 + //// /out/lib/the_package/_gleam_artefacts/three.cache_meta 34 + <77 byte binary> 35 + 36 + //// /out/lib/the_package/_gleam_artefacts/three.erl 37 + -module(three). 38 + -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]). 39 + -define(FILEPATH, "src/three.gleam"). 40 + -export([thing/0, escaped_thing/0]). 41 + 42 + -file("src/three.gleam", 2). 43 + -spec thing() -> nil. 44 + thing() -> 45 + thing:new(). 46 + 47 + -file("src/three.gleam", 6). 48 + -spec escaped_thing() -> nil. 49 + escaped_thing() -> 50 + 'the.thing':'make.new'(). 51 + 52 + 30 53 //// /out/lib/the_package/_gleam_artefacts/two.cache 31 54 <.cache binary> 32 55 33 56 //// /out/lib/the_package/_gleam_artefacts/two.cache_meta 34 - <487 byte binary> 57 + <489 byte binary> 35 58 36 59 //// /out/lib/the_package/_gleam_artefacts/two.erl 37 60 -module(two). ··· 131 154 {applications, []}, 132 155 {description, ""}, 133 156 {modules, [one, 157 + three, 134 158 two]}, 135 159 {registered, []} 136 160 ]}.
+30 -4
test-package-compiler/src/snapshots/test_package_compiler__generated_tests__imported_record_constructors.snap
··· 25 25 26 26 27 27 28 + //// /out/lib/the_package/_gleam_artefacts/one@two.cache 29 + <.cache binary> 30 + 31 + //// /out/lib/the_package/_gleam_artefacts/one@two.cache_meta 32 + <97 byte binary> 33 + 34 + //// /out/lib/the_package/_gleam_artefacts/one@two.erl 35 + -module(one@two). 36 + -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]). 37 + -define(FILEPATH, "src/one/two.gleam"). 38 + -export_type([a/0, b/0, user/0]). 39 + 40 + -type a() :: a. 41 + 42 + -type b() :: {b, a(), a()}. 43 + 44 + -type user() :: {user, binary(), integer()}. 45 + 46 + 47 + 48 + 28 49 //// /out/lib/the_package/_gleam_artefacts/two.cache 29 50 <.cache binary> 30 51 ··· 64 85 {Name, Score}. 65 86 66 87 -file("src/two.gleam", 70). 67 - -spec destructure_qualified_aliased(one@one:user()) -> {binary(), integer()}. 88 + -spec destructure_qualified_aliased(one@two:user()) -> {binary(), integer()}. 68 89 destructure_qualified_aliased(User) -> 69 90 {user, Name, Score} = User, 70 91 {Name, Score}. ··· 89 110 {user, <<"wibble"/utf8>>, erlang:element(3, _record)}. 90 111 91 112 -file("src/two.gleam", 90). 92 - -spec update_qualified_aliased(one@one:user()) -> one@one:user(). 113 + -spec update_qualified_aliased(one@two:user()) -> one@two:user(). 93 114 update_qualified_aliased(User) -> 94 115 _record = User, 95 116 {user, <<"wibble"/utf8>>, erlang:element(3, _record)}. ··· 117 138 {b, a, a}. 118 139 119 140 -file("src/two.gleam", 19). 120 - -spec qualified_aliased_fn_a() -> one@one:a(). 141 + -spec qualified_aliased_fn_a() -> one@two:a(). 121 142 qualified_aliased_fn_a() -> 122 143 a. 123 144 124 145 -file("src/two.gleam", 25). 125 - -spec qualified_aliased_fn_b() -> one@one:b(). 146 + -spec qualified_aliased_fn_b() -> one@two:b(). 126 147 qualified_aliased_fn_b() -> 127 148 {b, a, a}. 128 149 ··· 153 174 {applications, []}, 154 175 {description, ""}, 155 176 {modules, [one@one, 177 + one@two, 156 178 two]}, 157 179 {registered, []} 158 180 ]}. ··· 160 182 161 183 //// /out/lib/the_package/include/one@one_User.hrl 162 184 -record(user, {name :: binary(), score :: integer()}). 185 + 186 + 187 + //// /out/lib/the_package/include/one@two_User.hrl 188 + -record(user, {name :: binary(), score :: integer()}).