Fork of daniellemaywood.uk/gleam — Wasm codegen work
56 kB
1397 lines
1use crate::{
2 ast::{BitArraySegmentTruncation, SrcSpan, TodoKind},
3 build::Target,
4 diagnostic::{self, Diagnostic, ExtraLabel, Location},
5 error::wrap,
6 type_::{
7 self,
8 error::{
9 FeatureKind, LiteralCollectionKind, PanicPosition, TodoOrPanic,
10 UnreachablePatternReason,
11 },
12 expression::ComparisonOutcome,
13 pretty::Printer,
14 },
15};
16use camino::Utf8PathBuf;
17use debug_ignore::DebugIgnore;
18use ecow::EcoString;
19use std::{
20 io::Write,
21 sync::{Arc, atomic::Ordering},
22};
23use std::{rc::Rc, sync::atomic::AtomicUsize};
24use termcolor::Buffer;
25
26pub trait WarningEmitterIO {
27 fn emit_warning(&self, warning: Warning);
28}
29
30#[derive(Debug, Clone, Copy)]
31pub struct NullWarningEmitterIO;
32
33impl WarningEmitterIO for NullWarningEmitterIO {
34 fn emit_warning(&self, _warning: Warning) {}
35}
36
37#[derive(Debug, Clone, Default)]
38pub struct VectorWarningEmitterIO {
39 pub warnings: Arc<std::sync::RwLock<Vec<Warning>>>,
40}
41
42impl VectorWarningEmitterIO {
43 pub fn new() -> Self {
44 Self::default()
45 }
46
47 pub fn take(&self) -> Vec<Warning> {
48 let mut warnings = self.write_lock();
49 std::mem::take(&mut *warnings)
50 }
51
52 pub fn reset(&self) {
53 let mut warnings = self.write_lock();
54 warnings.clear();
55 }
56
57 pub fn pop(&self) -> Option<Warning> {
58 let mut warnings = self.write_lock();
59 warnings.pop()
60 }
61
62 fn write_lock(&self) -> std::sync::RwLockWriteGuard<'_, Vec<Warning>> {
63 self.warnings.write().expect("Vector lock poisoned")
64 }
65}
66
67impl WarningEmitterIO for VectorWarningEmitterIO {
68 fn emit_warning(&self, warning: Warning) {
69 let mut warnings = self.write_lock();
70 warnings.push(warning);
71 }
72}
73
74#[derive(Debug, Clone)]
75pub struct WarningEmitter {
76 /// The number of warnings emitted.
77 /// In the context of the project compiler this is the count for the root
78 /// package only, the count is reset back to zero after the dependencies are
79 /// compiled.
80 count: Arc<AtomicUsize>,
81 emitter: DebugIgnore<Rc<dyn WarningEmitterIO>>,
82}
83
84impl WarningEmitter {
85 pub fn new(emitter: Rc<dyn WarningEmitterIO>) -> Self {
86 Self {
87 count: Arc::new(AtomicUsize::new(0)),
88 emitter: DebugIgnore(emitter),
89 }
90 }
91
92 pub fn null() -> Self {
93 Self::new(Rc::new(NullWarningEmitterIO))
94 }
95
96 pub fn reset_count(&self) {
97 self.count.store(0, Ordering::Relaxed);
98 }
99
100 pub fn count(&self) -> usize {
101 self.count.load(Ordering::Relaxed)
102 }
103
104 pub fn emit(&self, warning: Warning) {
105 _ = self.count.fetch_add(1, Ordering::Relaxed);
106 self.emitter.emit_warning(warning);
107 }
108
109 pub fn vector() -> (Self, Rc<VectorWarningEmitterIO>) {
110 let io = Rc::new(VectorWarningEmitterIO::default());
111 let emitter = Self::new(io.clone());
112 (emitter, Rc::clone(&io))
113 }
114}
115
116#[derive(Debug, Clone)]
117pub struct TypeWarningEmitter {
118 module_path: Utf8PathBuf,
119 module_src: EcoString,
120 emitter: WarningEmitter,
121}
122
123impl TypeWarningEmitter {
124 pub fn new(module_path: Utf8PathBuf, module_src: EcoString, emitter: WarningEmitter) -> Self {
125 Self {
126 module_path,
127 module_src,
128 emitter,
129 }
130 }
131
132 pub fn null() -> Self {
133 Self {
134 module_path: Utf8PathBuf::new(),
135 module_src: EcoString::from(""),
136 emitter: WarningEmitter::new(Rc::new(NullWarningEmitterIO)),
137 }
138 }
139
140 pub fn emit(&self, warning: type_::Warning) {
141 self.emitter.emit(Warning::Type {
142 path: self.module_path.clone(),
143 src: self.module_src.clone(),
144 warning,
145 });
146 }
147}
148
149#[derive(Debug, Clone, Eq, PartialEq)]
150pub enum Warning {
151 Type {
152 path: Utf8PathBuf,
153 src: EcoString,
154 warning: type_::Warning,
155 },
156
157 InvalidSource {
158 path: Utf8PathBuf,
159 },
160
161 DeprecatedSyntax {
162 path: Utf8PathBuf,
163 src: EcoString,
164 warning: DeprecatedSyntaxWarning,
165 },
166
167 DeprecatedEnvironmentVariable {
168 variable: DeprecatedEnvironmentVariable,
169 },
170}
171
172#[derive(Debug, Clone, Eq, PartialEq, Copy)]
173pub enum DeprecatedEnvironmentVariable {
174 HexpmUser,
175 HexpmPass,
176}
177
178impl DeprecatedEnvironmentVariable {
179 fn name(&self) -> &'static str {
180 match self {
181 DeprecatedEnvironmentVariable::HexpmUser => "HEXPM_USER",
182 DeprecatedEnvironmentVariable::HexpmPass => "HEXPM_PASS",
183 }
184 }
185
186 fn message(&self) -> &'static str {
187 match self {
188 DeprecatedEnvironmentVariable::HexpmUser => {
189 "Use the `{API_ENV_NAME}` environment variable instead."
190 }
191 DeprecatedEnvironmentVariable::HexpmPass => {
192 "Use the `{API_ENV_NAME}` environment variable instead."
193 }
194 }
195 }
196}
197
198#[derive(Debug, Clone, Eq, PartialEq, Copy)]
199pub enum DeprecatedSyntaxWarning {
200 /// If someone uses the deprecated syntax to append to a list:
201 /// `["a"..rest]`, notice how there's no comma!
202 DeprecatedListPrepend {
203 location: SrcSpan,
204 },
205
206 /// If someone uses the deprecated syntax to pattern match on a list:
207 /// ```gleam
208 /// case list {
209 /// [first..rest] -> todo
210 /// // ^^ notice there's no comma!
211 /// _ ->
212 /// }
213 /// ```
214 ///
215 DeprecatedListPattern {
216 location: SrcSpan,
217 },
218
219 /// If someone uses the deprecated syntax to match on all lists instead of
220 /// a common `_`:
221 /// ```gleam
222 /// case list {
223 /// [..] -> todo
224 /// //^^^^ this matches on all lists so a `_` should be used instead!
225 /// _ ->
226 /// }
227 /// ```
228 ///
229 DeprecatedListCatchAllPattern {
230 location: SrcSpan,
231 },
232
233 /// If a record pattern has a spread that is not preceded by a comma:
234 /// ```gleam
235 /// case wibble {
236 /// Wibble(arg1: name ..) -> todo
237 /// // ^^ this should be preceded by a comma!
238 /// }
239 /// ```
240 ///
241 DeprecatedRecordSpreadPattern {
242 location: SrcSpan,
243 },
244
245 DeprecatedTargetShorthand {
246 target: Target,
247 location: SrcSpan,
248 },
249}
250
251impl Warning {
252 pub fn to_diagnostic(&self) -> Diagnostic {
253 match self {
254 Warning::InvalidSource { path } => Diagnostic {
255 title: "Invalid module name".into(),
256 text: "\
257Module names must begin with a lowercase letter and contain
258only lowercase alphanumeric characters or underscores."
259 .into(),
260 level: diagnostic::Level::Warning,
261 location: None,
262 hint: Some(format!(
263 "Rename `{path}` to be valid, or remove this file from the project source."
264 )),
265 },
266
267 Warning::DeprecatedSyntax {
268 path,
269 src,
270 warning: DeprecatedSyntaxWarning::DeprecatedListPrepend { location },
271 } => Diagnostic {
272 title: "Deprecated prepend syntax".into(),
273 text: wrap(
274 "This syntax for prepending to a list is deprecated.
275When prepending an item to a list it should be preceded by a comma, \
276like this: `[item, ..list]`.",
277 ),
278
279 hint: None,
280 level: diagnostic::Level::Warning,
281 location: Some(Location {
282 label: diagnostic::Label {
283 text: Some("This spread should be preceded by a comma".into()),
284 span: *location,
285 },
286 path: path.clone(),
287 src: src.clone(),
288 extra_labels: vec![],
289 }),
290 },
291
292 Warning::DeprecatedSyntax {
293 path,
294 src,
295 warning: DeprecatedSyntaxWarning::DeprecatedListPattern { location },
296 } => Diagnostic {
297 title: "Deprecated list pattern matching syntax".into(),
298 text: wrap(
299 "This syntax for pattern matching on a list is deprecated.
300When matching on the rest of a list it should always be preceded by a comma, \
301like this: `[item, ..list]`.",
302 ),
303 hint: None,
304 level: diagnostic::Level::Warning,
305 location: Some(Location {
306 label: diagnostic::Label {
307 text: Some("This spread should be preceded by a comma".into()),
308 span: *location,
309 },
310 path: path.clone(),
311 src: src.clone(),
312 extra_labels: vec![],
313 }),
314 },
315
316 Warning::DeprecatedSyntax {
317 path,
318 src,
319 warning: DeprecatedSyntaxWarning::DeprecatedRecordSpreadPattern { location },
320 } => Diagnostic {
321 title: "Deprecated record pattern matching syntax".into(),
322 text: wrap("This syntax for pattern matching on a record is deprecated."),
323 hint: None,
324 level: diagnostic::Level::Warning,
325 location: Some(Location {
326 label: diagnostic::Label {
327 text: Some("This should be preceded by a comma".into()),
328 span: *location,
329 },
330 path: path.clone(),
331 src: src.clone(),
332 extra_labels: vec![],
333 }),
334 },
335
336 Warning::DeprecatedSyntax {
337 path,
338 src,
339 warning: DeprecatedSyntaxWarning::DeprecatedListCatchAllPattern { location },
340 } => Diagnostic {
341 title: "Deprecated list pattern matching syntax".into(),
342 text: wrap(
343 "This syntax for pattern matching on lists is deprecated.
344To match on all possible lists, use the `_` catch-all pattern instead.",
345 ),
346 hint: None,
347 level: diagnostic::Level::Warning,
348 location: Some(Location {
349 label: diagnostic::Label {
350 text: Some("This can be replaced with `_`".into()),
351 span: *location,
352 },
353 path: path.clone(),
354 src: src.clone(),
355 extra_labels: vec![],
356 }),
357 },
358
359 Warning::DeprecatedSyntax {
360 path,
361 src,
362 warning: DeprecatedSyntaxWarning::DeprecatedTargetShorthand { location, target },
363 } => {
364 let full_name = match target {
365 Target::Erlang => "erlang",
366 Target::JavaScript => "javascript",
367 };
368
369 Diagnostic {
370 title: "Deprecated target shorthand syntax".into(),
371 text: wrap(&format!(
372 "This shorthand target name is deprecated. Use the full name: `{full_name}` instead."
373 )),
374 hint: None,
375 level: diagnostic::Level::Warning,
376 location: Some(Location {
377 label: diagnostic::Label {
378 text: Some(format!("This should be replaced with `{full_name}`")),
379 span: *location,
380 },
381 path: path.clone(),
382 src: src.clone(),
383 extra_labels: vec![],
384 }),
385 }
386 }
387
388 Warning::Type { path, warning, src } => match warning {
389 type_::Warning::Todo {
390 kind,
391 location,
392 type_,
393 } => {
394 let mut text = String::new();
395 text.push_str(
396 "\
397This code will crash if it is run. Be sure to finish it before
398running your program.",
399 );
400 let title = match kind {
401 TodoKind::Keyword => "Todo found",
402 TodoKind::EmptyBlock => {
403 text.push_str(
404 "
405A block must always contain at least one expression.",
406 );
407 "Incomplete block"
408 }
409 TodoKind::EmptyFunction { .. } => "Unimplemented function",
410 TodoKind::IncompleteUse => {
411 text.push_str(
412 "
413A use expression must always be followed by at least one expression.",
414 );
415 "Incomplete use expression"
416 }
417 }
418 .into();
419 if !type_.is_variable() {
420 text.push_str(&format!(
421 "\n\nHint: I think its type is `{}`.\n",
422 Printer::new().pretty_print(type_, 0)
423 ));
424 }
425
426 Diagnostic {
427 title,
428 text,
429 level: diagnostic::Level::Warning,
430 location: Some(Location {
431 path: path.to_path_buf(),
432 src: src.clone(),
433 label: diagnostic::Label {
434 text: Some("This code is incomplete".into()),
435 span: *location,
436 },
437 extra_labels: Vec::new(),
438 }),
439 hint: None,
440 }
441 }
442
443 type_::Warning::ImplicitlyDiscardedResult { location } => Diagnostic {
444 title: "Unused result value".into(),
445 text: "".into(),
446 hint: Some(
447 "If you are sure you don't need it you can assign it to `_`.".into(),
448 ),
449 level: diagnostic::Level::Warning,
450 location: Some(Location {
451 path: path.to_path_buf(),
452 src: src.clone(),
453 label: diagnostic::Label {
454 text: Some("The Result value created here is unused".into()),
455 span: *location,
456 },
457 extra_labels: Vec::new(),
458 }),
459 },
460
461 type_::Warning::UnusedLiteral { location } => Diagnostic {
462 title: "Unused literal".into(),
463 text: "".into(),
464 hint: Some("You can safely remove it.".into()),
465 level: diagnostic::Level::Warning,
466 location: Some(Location {
467 path: path.to_path_buf(),
468 src: src.clone(),
469 label: diagnostic::Label {
470 text: Some("This value is never used".into()),
471 span: *location,
472 },
473 extra_labels: Vec::new(),
474 }),
475 },
476
477 type_::Warning::NoFieldsRecordUpdate { location } => Diagnostic {
478 title: "Fieldless record update".into(),
479 text: "".into(),
480 hint: Some(
481 "Add some fields to change or replace it with the record itself.".into(),
482 ),
483 level: diagnostic::Level::Warning,
484 location: Some(Location {
485 path: path.to_path_buf(),
486 src: src.clone(),
487 label: diagnostic::Label {
488 text: Some("This record update doesn't change any fields".into()),
489 span: *location,
490 },
491 extra_labels: Vec::new(),
492 }),
493 },
494
495 type_::Warning::AllFieldsRecordUpdate { location } => Diagnostic {
496 title: "Redundant record update".into(),
497 text: "".into(),
498 hint: Some("It is better style to use the record creation syntax.".into()),
499 level: diagnostic::Level::Warning,
500 location: Some(Location {
501 src: src.clone(),
502 path: path.to_path_buf(),
503 label: diagnostic::Label {
504 text: Some("This record update specifies all fields".into()),
505 span: *location,
506 },
507 extra_labels: Vec::new(),
508 }),
509 },
510
511 type_::Warning::UnusedType {
512 location, imported, ..
513 } => {
514 let title = if *imported {
515 "Unused imported type".into()
516 } else {
517 "Unused private type".into()
518 };
519 let label = if *imported {
520 "This imported type is never used".into()
521 } else {
522 "This private type is never used".into()
523 };
524 Diagnostic {
525 title,
526 text: "".into(),
527 hint: Some("You can safely remove it.".into()),
528 level: diagnostic::Level::Warning,
529 location: Some(Location {
530 src: src.clone(),
531 path: path.to_path_buf(),
532 label: diagnostic::Label {
533 text: Some(label),
534 span: *location,
535 },
536 extra_labels: Vec::new(),
537 }),
538 }
539 }
540
541 type_::Warning::UnusedConstructor {
542 location, imported, ..
543 } => {
544 let title = if *imported {
545 "Unused imported item".into()
546 } else {
547 "Unused private constructor".into()
548 };
549 let label = if *imported {
550 "This imported constructor is never used".into()
551 } else {
552 "This private constructor is never used".into()
553 };
554 Diagnostic {
555 title,
556 text: "".into(),
557 hint: Some("You can safely remove it.".into()),
558 level: diagnostic::Level::Warning,
559 location: Some(Location {
560 src: src.clone(),
561 path: path.to_path_buf(),
562 label: diagnostic::Label {
563 text: Some(label),
564 span: *location,
565 },
566 extra_labels: Vec::new(),
567 }),
568 }
569 }
570
571 type_::Warning::UnusedImportedModule { location, .. } => Diagnostic {
572 title: "Unused imported module".into(),
573 text: "".into(),
574 hint: Some("You can safely remove it.".into()),
575 level: diagnostic::Level::Warning,
576 location: Some(Location {
577 src: src.clone(),
578 path: path.to_path_buf(),
579 label: diagnostic::Label {
580 text: Some("This imported module is never used".into()),
581 span: *location,
582 },
583 extra_labels: Vec::new(),
584 }),
585 },
586
587 type_::Warning::UnusedImportedModuleAlias {
588 location,
589 module_name,
590 ..
591 } => {
592 let text = format!(
593 "\
594Hint: You can safely remove it.
595
596 import {module_name} as _
597"
598 );
599 Diagnostic {
600 title: "Unused imported module alias".into(),
601 text,
602 hint: None,
603 level: diagnostic::Level::Warning,
604 location: Some(Location {
605 src: src.clone(),
606 path: path.to_path_buf(),
607 label: diagnostic::Label {
608 text: Some("This alias is never used".into()),
609 span: *location,
610 },
611 extra_labels: Vec::new(),
612 }),
613 }
614 }
615
616 type_::Warning::UnusedImportedValue { location, .. } => Diagnostic {
617 title: "Unused imported value".into(),
618 text: "".into(),
619 hint: Some("You can safely remove it.".into()),
620 level: diagnostic::Level::Warning,
621 location: Some(Location {
622 src: src.clone(),
623 path: path.to_path_buf(),
624 label: diagnostic::Label {
625 text: Some("This imported value is never used".into()),
626 span: *location,
627 },
628 extra_labels: Vec::new(),
629 }),
630 },
631
632 type_::Warning::UnusedPrivateModuleConstant { location, .. } => Diagnostic {
633 title: "Unused private constant".into(),
634 text: "".into(),
635 hint: Some("You can safely remove it.".into()),
636 level: diagnostic::Level::Warning,
637 location: Some(Location {
638 src: src.clone(),
639 path: path.to_path_buf(),
640 label: diagnostic::Label {
641 text: Some("This private constant is never used".into()),
642 span: *location,
643 },
644 extra_labels: Vec::new(),
645 }),
646 },
647
648 type_::Warning::UnusedPrivateFunction { location, .. } => Diagnostic {
649 title: "Unused private function".into(),
650 text: "".into(),
651 hint: Some("You can safely remove it.".into()),
652 level: diagnostic::Level::Warning,
653 location: Some(Location {
654 src: src.clone(),
655 path: path.to_path_buf(),
656 label: diagnostic::Label {
657 text: Some("This private function is never used".into()),
658 span: *location,
659 },
660 extra_labels: Vec::new(),
661 }),
662 },
663
664 type_::Warning::UnusedVariable { location, origin } => Diagnostic {
665 title: "Unused variable".into(),
666 text: "".into(),
667 hint: origin.how_to_ignore(),
668 level: diagnostic::Level::Warning,
669 location: Some(Location {
670 src: src.clone(),
671 path: path.to_path_buf(),
672 label: diagnostic::Label {
673 text: Some("This variable is never used".into()),
674 span: *location,
675 },
676 extra_labels: Vec::new(),
677 }),
678 },
679 type_::Warning::UnnecessaryDoubleIntNegation { location } => Diagnostic {
680 title: "Unnecessary double negation (--) on integer".into(),
681 text: "".into(),
682 hint: None,
683 level: diagnostic::Level::Warning,
684 location: Some(Location {
685 src: src.clone(),
686 path: path.to_path_buf(),
687 label: diagnostic::Label {
688 text: Some("You can safely remove this.".into()),
689 span: *location,
690 },
691 extra_labels: Vec::new(),
692 }),
693 },
694 type_::Warning::UnnecessaryDoubleBoolNegation { location } => Diagnostic {
695 title: "Unnecessary double negation (!!) on bool".into(),
696 text: "".into(),
697 hint: None,
698 level: diagnostic::Level::Warning,
699 location: Some(Location {
700 src: src.clone(),
701 path: path.to_path_buf(),
702 label: diagnostic::Label {
703 text: Some("You can safely remove this.".into()),
704 span: *location,
705 },
706 extra_labels: Vec::new(),
707 }),
708 },
709 type_::Warning::InefficientEmptyListCheck { location, kind } => {
710 use type_::error::EmptyListCheckKind;
711 let text = "The `list.length` function has to iterate across the whole
712list to calculate the length, which is wasteful if you only
713need to know if the list is empty or not.
714"
715 .into();
716 let hint = Some(match kind {
717 EmptyListCheckKind::Empty => "You can use `the_list == []` instead.".into(),
718 EmptyListCheckKind::NonEmpty => {
719 "You can use `the_list != []` instead.".into()
720 }
721 });
722
723 Diagnostic {
724 title: "Inefficient use of `list.length`".into(),
725 text,
726 hint,
727 level: diagnostic::Level::Warning,
728 location: Some(Location {
729 src: src.clone(),
730 path: path.to_path_buf(),
731 label: diagnostic::Label {
732 text: None,
733 span: *location,
734 },
735 extra_labels: Vec::new(),
736 }),
737 }
738 }
739
740 type_::Warning::TransitiveDependencyImported {
741 location,
742 module,
743 package,
744 } => {
745 let text = wrap(&format!(
746 "The module `{module}` is being imported, but \
747`{package}`, the package it belongs to, is not a direct dependency of your \
748package.
749In a future version of Gleam this may become a compile error.
750
751Run this command to add it to your dependencies:
752
753 gleam add {package}
754"
755 ));
756 Diagnostic {
757 title: "Transitive dependency imported".into(),
758 text,
759 hint: None,
760 level: diagnostic::Level::Warning,
761 location: Some(Location {
762 src: src.clone(),
763 path: path.to_path_buf(),
764 label: diagnostic::Label {
765 text: None,
766 span: *location,
767 },
768 extra_labels: Vec::new(),
769 }),
770 }
771 }
772
773 type_::Warning::DeprecatedItem {
774 location,
775 message,
776 layer,
777 } => {
778 let text = wrap(&format!("It was deprecated with this message: {message}"));
779 let (title, diagnostic_label_text) = if layer.is_value() {
780 (
781 "Deprecated value used".into(),
782 Some("This value has been deprecated".into()),
783 )
784 } else {
785 (
786 "Deprecated type used".into(),
787 Some("This type has been deprecated".into()),
788 )
789 };
790
791 Diagnostic {
792 title,
793 text,
794 hint: None,
795 level: diagnostic::Level::Warning,
796 location: Some(Location {
797 src: src.clone(),
798 path: path.to_path_buf(),
799 label: diagnostic::Label {
800 text: diagnostic_label_text,
801 span: *location,
802 },
803 extra_labels: Vec::new(),
804 }),
805 }
806 }
807
808 type_::Warning::UnreachableCasePattern { location, reason } => {
809 let text: String = match reason {
810 UnreachablePatternReason::DuplicatePattern => wrap(
811 "This pattern cannot be reached as a previous \
812pattern matches the same values.\n",
813 ),
814 UnreachablePatternReason::ImpossibleVariant => wrap(
815 "This pattern cannot be reached as it matches on \
816a variant of a type which is never present.\n",
817 ),
818 };
819 Diagnostic {
820 title: "Unreachable pattern".into(),
821 text,
822 hint: Some("It can be safely removed.".into()),
823 level: diagnostic::Level::Warning,
824 location: Some(Location {
825 src: src.clone(),
826 path: path.to_path_buf(),
827 label: diagnostic::Label {
828 text: None,
829 span: *location,
830 },
831 extra_labels: Vec::new(),
832 }),
833 }
834 }
835
836 type_::Warning::CaseMatchOnLiteralCollection { kind, location } => {
837 let kind = match kind {
838 LiteralCollectionKind::List => "list",
839 LiteralCollectionKind::Tuple => "tuple",
840 LiteralCollectionKind::Record => "record",
841 };
842
843 let title = format!("Redundant {kind}");
844 let text = wrap(&format!(
845 "Instead of building a {kind} and matching on it, \
846you can match on its contents directly.
847A case expression can take multiple subjects separated by commas like this:
848
849 case one_subject, another_subject {{
850 _, _ -> todo
851 }}
852
853See: https://tour.gleam.run/flow-control/multiple-subjects/"
854 ));
855
856 Diagnostic {
857 title,
858 text,
859 hint: None,
860 level: diagnostic::Level::Warning,
861 location: Some(Location {
862 src: src.clone(),
863 path: path.to_path_buf(),
864 label: diagnostic::Label {
865 text: Some(format!("You can remove this {kind} wrapper")),
866 span: *location,
867 },
868 extra_labels: Vec::new(),
869 }),
870 }
871 }
872
873 type_::Warning::CaseMatchOnLiteralValue { location } => Diagnostic {
874 title: "Match on a literal value".into(),
875 text: wrap(
876 "Matching on a literal value is redundant since you \
877can already tell which branch is going to match with this value.",
878 ),
879 hint: None,
880 level: diagnostic::Level::Warning,
881 location: Some(Location {
882 src: src.clone(),
883 path: path.to_path_buf(),
884 label: diagnostic::Label {
885 text: Some("There's no need to pattern match on this value".into()),
886 span: *location,
887 },
888 extra_labels: Vec::new(),
889 }),
890 },
891
892 type_::Warning::OpaqueExternalType { location } => Diagnostic {
893 title: "Opaque external type".into(),
894 text: "This type has no constructors so making it opaque is redundant.".into(),
895 hint: Some("Remove the `opaque` qualifier from the type definition.".into()),
896 level: diagnostic::Level::Warning,
897 location: Some(Location {
898 src: src.clone(),
899 path: path.to_path_buf(),
900 label: diagnostic::Label {
901 text: None,
902 span: *location,
903 },
904 extra_labels: Vec::new(),
905 }),
906 },
907
908 type_::Warning::UnusedValue { location } => Diagnostic {
909 title: "Unused value".into(),
910 text: wrap(
911 "This expression computes a value without any side \
912effects, but then the value isn't used at all. You might want to assign it to a \
913variable, or delete the expression entirely if it's not needed.",
914 ),
915 hint: None,
916 level: diagnostic::Level::Warning,
917 location: Some(Location {
918 path: path.to_path_buf(),
919 src: src.clone(),
920 label: diagnostic::Label {
921 text: Some("This value is never used".into()),
922 span: *location,
923 },
924 extra_labels: Vec::new(),
925 }),
926 },
927
928 type_::Warning::InternalTypeLeak { location, leaked } => {
929 let mut printer = Printer::new();
930
931 // TODO: be more precise.
932 // - is being returned by this public function
933 // - is taken as an argument by this public function
934 // - is taken as an argument by this public enum constructor
935 // etc
936 let text = format!(
937 "The following type is internal, but is being used by this public export.
938
939{}
940
941Internal types should not be used in a public facing function since they are
942hidden from the package's documentation.",
943 printer.pretty_print(leaked, 4),
944 );
945 Diagnostic {
946 title: "Internal type used in public interface".into(),
947 text,
948 hint: None,
949 level: diagnostic::Level::Warning,
950 location: Some(Location {
951 label: diagnostic::Label {
952 text: None,
953 span: *location,
954 },
955 path: path.clone(),
956 src: src.clone(),
957 extra_labels: vec![],
958 }),
959 }
960 }
961 type_::Warning::RedundantAssertAssignment { location } => Diagnostic {
962 title: "Redundant assertion".into(),
963 text: "This assertion is redundant since the pattern covers all possibilities."
964 .into(),
965 hint: None,
966 level: diagnostic::Level::Warning,
967 location: Some(Location {
968 label: diagnostic::Label {
969 text: Some("You can remove this".into()),
970 span: *location,
971 },
972 path: path.clone(),
973 src: src.clone(),
974 extra_labels: vec![],
975 }),
976 },
977
978 type_::Warning::AssertAssignmentOnInferredVariant { location } => Diagnostic {
979 title: "Assertion that will always fail".into(),
980 text: wrap(
981 "We can tell from the code above that the value will never match \
982this pattern and that this code will always crash.
983
984Either change the pattern or use `panic` to unconditionally fail.",
985 ),
986 hint: None,
987 level: diagnostic::Level::Warning,
988 location: Some(Location {
989 label: diagnostic::Label {
990 text: None,
991 span: *location,
992 },
993 path: path.clone(),
994 src: src.clone(),
995 extra_labels: vec![],
996 }),
997 },
998
999 type_::Warning::TodoOrPanicUsedAsFunction {
1000 kind,
1001 location,
1002 arguments_location,
1003 arguments,
1004 } => {
1005 let title = match kind {
1006 TodoOrPanic::Todo => "Todo used as a function".into(),
1007 TodoOrPanic::Panic => "Panic used as a function".into(),
1008 };
1009 let label_location = match arguments_location {
1010 None => location,
1011 Some(location) => location,
1012 };
1013 let name = match kind {
1014 TodoOrPanic::Todo => "todo",
1015 TodoOrPanic::Panic => "panic",
1016 };
1017 let mut text = format!("`{name}` is not a function");
1018 match arguments {
1019 0 => text.push_str(&format!(
1020 ", you can just write `{name}` instead of `{name}()`."
1021 )),
1022 1 => text.push_str(
1023 " and will crash before it can do anything with this argument.",
1024 ),
1025 _ => text.push_str(
1026 " and will crash before it can do anything with these arguments.",
1027 ),
1028 };
1029
1030 match arguments {
1031 0 => {}
1032 _ => text.push_str(&format!(
1033 "\n\nHint: if you want to display an error message you should write
1034`{name} as \"my error message\"`
1035See: https://tour.gleam.run/advanced-features/{name}/"
1036 )),
1037 }
1038
1039 Diagnostic {
1040 title,
1041 text: wrap(&text),
1042 hint: None,
1043 level: diagnostic::Level::Warning,
1044 location: Some(Location {
1045 label: diagnostic::Label {
1046 text: None,
1047 span: *label_location,
1048 },
1049 path: path.clone(),
1050 src: src.clone(),
1051 extra_labels: vec![],
1052 }),
1053 }
1054 }
1055
1056 type_::Warning::UnreachableCodeAfterPanic {
1057 location,
1058 panic_position: unreachable_code_kind,
1059 } => {
1060 let text = match unreachable_code_kind {
1061 PanicPosition::PreviousExpression => {
1062 "This code is unreachable because it comes after a `panic`."
1063 }
1064 PanicPosition::PreviousFunctionArgument => {
1065 "This argument is unreachable because the previous one always panics. \
1066Your code will crash before reaching this point."
1067 }
1068 PanicPosition::LastFunctionArgument => {
1069 "This function call is unreachable because its last argument always panics. \
1070Your code will crash before reaching this point."
1071 }
1072 PanicPosition::EchoExpression => {
1073 "This `echo` won't print anything because the expression it \
1074should be printing always panics."
1075 }
1076 };
1077
1078 Diagnostic {
1079 title: "Unreachable code".into(),
1080 text: wrap(text),
1081 hint: None,
1082 level: diagnostic::Level::Warning,
1083 location: Some(Location {
1084 label: diagnostic::Label {
1085 text: None,
1086 span: *location,
1087 },
1088 path: path.clone(),
1089 src: src.clone(),
1090 extra_labels: vec![],
1091 }),
1092 }
1093 }
1094
1095 type_::Warning::RedundantPipeFunctionCapture { location } => Diagnostic {
1096 title: "Redundant function capture".into(),
1097 text: wrap(
1098 "This function capture is redundant since the value is already piped as \
1099the first argument of this call.
1100
1101See: https://tour.gleam.run/functions/pipelines/",
1102 ),
1103 hint: None,
1104 level: diagnostic::Level::Warning,
1105 location: Some(Location {
1106 label: diagnostic::Label {
1107 text: Some("You can safely remove this".into()),
1108 span: *location,
1109 },
1110 path: path.clone(),
1111 src: src.clone(),
1112 extra_labels: vec![],
1113 }),
1114 },
1115 type_::Warning::FeatureRequiresHigherGleamVersion {
1116 location,
1117 minimum_required_version,
1118 wrongfully_allowed_version,
1119 feature_kind,
1120 } => {
1121 let feature = match feature_kind {
1122 FeatureKind::LabelShorthandSyntax => "The label shorthand syntax was",
1123 FeatureKind::ConstantStringConcatenation => {
1124 "Constant strings concatenation was"
1125 }
1126 FeatureKind::ArithmeticInGuards => "Arithmetic operations in guards were",
1127 FeatureKind::UnannotatedUtf8StringSegment => {
1128 "The ability to omit the `utf8` annotation for string segments was"
1129 }
1130 FeatureKind::UnannotatedFloatSegment => {
1131 "The ability to omit the `float` annotation for float segments was"
1132 }
1133 FeatureKind::NestedTupleAccess => {
1134 "The ability to access nested tuple fields was"
1135 }
1136 FeatureKind::InternalAnnotation => "The `@internal` annotation was",
1137 FeatureKind::AtInJavascriptModules => {
1138 "The ability to have `@` in a Javascript module's name was"
1139 }
1140 FeatureKind::RecordUpdateVariantInference => {
1141 "Record updates for custom types when the variant is known was"
1142 }
1143 FeatureKind::RecordAccessVariantInference => {
1144 "Field access on custom types when the variant is known was"
1145 }
1146 FeatureKind::LetAssertWithMessage => {
1147 "Specifying a custom panic message when using let assert was"
1148 }
1149 FeatureKind::VariantWithDeprecatedAnnotation => {
1150 "Deprecating individual custom type variants was"
1151 }
1152 FeatureKind::JavaScriptUnalignedBitArray => {
1153 "Use of unaligned bit arrays on the JavaScript target was"
1154 }
1155 FeatureKind::BoolAssert => "The bool `assert` statement was",
1156 };
1157
1158 Diagnostic {
1159 title: "Incompatible gleam version range".into(),
1160 text: wrap(&format!(
1161 "{feature} introduced in version v{minimum_required_version}. But the Gleam version range \
1162 specified in your `gleam.toml` would allow this code to run on an earlier \
1163 version like v{wrongfully_allowed_version}, resulting in compilation errors!",
1164 )),
1165 hint: Some(format!(
1166 "Remove the version constraint from your `gleam.toml` or update it to be:
1167
1168 gleam = \">= {minimum_required_version}\""
1169 )),
1170 level: diagnostic::Level::Warning,
1171 location: Some(Location {
1172 label: diagnostic::Label {
1173 text: Some(format!(
1174 "This requires a Gleam version >= {minimum_required_version}"
1175 )),
1176 span: *location,
1177 },
1178 path: path.clone(),
1179 src: src.clone(),
1180 extra_labels: vec![],
1181 }),
1182 }
1183 }
1184
1185 type_::Warning::JavaScriptIntUnsafe { location } => Diagnostic {
1186 title: "Int is outside JavaScript's safe integer range".into(),
1187 text: wrap(
1188 "This integer value is too large to be represented accurately by \
1189JavaScript's number type. To avoid this warning integer values must be in the range \
1190-(2^53 - 1) - (2^53 - 1).
1191
1192See JavaScript's Number.MAX_SAFE_INTEGER and Number.MIN_SAFE_INTEGER properties for more \
1193information.",
1194 ),
1195 hint: None,
1196 level: diagnostic::Level::Warning,
1197 location: Some(Location {
1198 path: path.to_path_buf(),
1199 src: src.clone(),
1200 label: diagnostic::Label {
1201 text: Some("This is not a safe integer value on JavaScript".into()),
1202 span: *location,
1203 },
1204 extra_labels: Vec::new(),
1205 }),
1206 },
1207
1208 type_::Warning::BitArraySegmentTruncatedValue {
1209 location: _,
1210 truncation:
1211 BitArraySegmentTruncation {
1212 truncated_value,
1213 truncated_into,
1214 segment_bits,
1215 value_location,
1216 },
1217 } => {
1218 let (unit, segment_size, taken) = if segment_bits % 8 == 0 {
1219 let bytes = segment_bits / 8;
1220 let segment_size = pluralise(format!("{bytes} byte"), bytes);
1221 let taken = if bytes == 1 {
1222 "first byte".into()
1223 } else {
1224 format!("first {bytes} bytes")
1225 };
1226
1227 ("bytes", segment_size, taken)
1228 } else {
1229 let segment_size = pluralise(format!("{segment_bits} bit"), *segment_bits);
1230 let taken = if *segment_bits == 1 {
1231 "first bit".into()
1232 } else {
1233 format!("first {segment_bits} bits")
1234 };
1235 ("bits", segment_size, taken)
1236 };
1237
1238 let text = format!(
1239 "This segment is {segment_size} long, but {truncated_value} \
1240doesn't fit in that many {unit}. It would be truncated by taking its {taken}, resulting in the value {truncated_into}."
1241 );
1242
1243 Diagnostic {
1244 title: "Truncated bit array segment".into(),
1245 text: wrap(&text),
1246 hint: None,
1247 level: diagnostic::Level::Warning,
1248 location: Some(Location {
1249 path: path.to_path_buf(),
1250 src: src.clone(),
1251 label: diagnostic::Label {
1252 text: Some(format!(
1253 "You can safely replace this with {truncated_into}"
1254 )),
1255 span: *value_location,
1256 },
1257 extra_labels: vec![],
1258 }),
1259 }
1260 }
1261
1262 type_::Warning::AssertLiteralBool { location } => Diagnostic {
1263 title: "Assertion of a literal value".into(),
1264 text: wrap(
1265 "Asserting on a literal bool is redundant since you \
1266can already tell whether it will be `True` or `False`.",
1267 ),
1268 hint: None,
1269 level: diagnostic::Level::Warning,
1270 location: Some(Location {
1271 src: src.clone(),
1272 path: path.to_path_buf(),
1273 label: diagnostic::Label {
1274 text: None,
1275 span: *location,
1276 },
1277 extra_labels: Vec::new(),
1278 }),
1279 },
1280
1281 type_::Warning::ModuleImportedTwice {
1282 name,
1283 first,
1284 second,
1285 } => Diagnostic {
1286 title: "Duplicate import".into(),
1287 text: format!("The {name} module has been imported twice."),
1288 hint: None,
1289 level: diagnostic::Level::Warning,
1290 location: Some(Location {
1291 src: src.clone(),
1292 path: path.to_path_buf(),
1293 label: diagnostic::Label {
1294 text: Some("Reimported here".into()),
1295 span: *second,
1296 },
1297 extra_labels: vec![ExtraLabel {
1298 src_info: None,
1299 label: diagnostic::Label {
1300 text: Some("First imported here".into()),
1301 span: *first,
1302 },
1303 }],
1304 }),
1305 },
1306
1307 type_::Warning::TopLevelDefinitionShadowsImport { location, name } => {
1308 let text = format!(
1309 "Definition of {name} shadows an imported value.
1310The imported value could not be used in this module anyway."
1311 );
1312 Diagnostic {
1313 title: "Shadowed Import".into(),
1314 text: wrap(&text),
1315 level: diagnostic::Level::Warning,
1316 location: Some(Location {
1317 path: path.clone(),
1318 src: src.clone(),
1319 label: diagnostic::Label {
1320 text: Some(wrap(&format!("`{name}` is defined here"))),
1321 span: *location,
1322 },
1323 extra_labels: Vec::new(),
1324 }),
1325 hint: Some("Either rename the definition or remove the import.".into()),
1326 }
1327 }
1328
1329 type_::Warning::RedundantComparison { location, outcome } => Diagnostic {
1330 title: "Redundant comparison".into(),
1331 text: format!(
1332 "This comparison is redundant since it always {}.",
1333 match outcome {
1334 ComparisonOutcome::AlwaysSucceeds => "succeeds",
1335 ComparisonOutcome::AlwaysFails => "fails",
1336 }
1337 ),
1338 hint: None,
1339 level: diagnostic::Level::Warning,
1340 location: Some(Location {
1341 label: diagnostic::Label {
1342 text: Some(format!(
1343 "This is always `{}`",
1344 match outcome {
1345 ComparisonOutcome::AlwaysSucceeds => "True",
1346 ComparisonOutcome::AlwaysFails => "False",
1347 }
1348 )),
1349 span: *location,
1350 },
1351 path: path.clone(),
1352 src: src.clone(),
1353 extra_labels: vec![],
1354 }),
1355 },
1356 },
1357
1358 Warning::DeprecatedEnvironmentVariable { variable } => {
1359 let name = variable.name();
1360 let message = variable.message();
1361
1362 let text = wrap(&format!(
1363 "The environment variable `{name}` is deprecated.\n\n{message}"
1364 ));
1365
1366 Diagnostic {
1367 title: "Use of deprecated environment variable".into(),
1368 text,
1369 hint: None,
1370 level: diagnostic::Level::Warning,
1371 location: None,
1372 }
1373 }
1374 }
1375 }
1376
1377 pub fn pretty(&self, buffer: &mut Buffer) {
1378 self.to_diagnostic().write(buffer);
1379 buffer
1380 .write_all(b"\n")
1381 .expect("error pretty buffer write space after");
1382 }
1383
1384 pub fn to_pretty_string(&self) -> String {
1385 let mut nocolor = Buffer::no_color();
1386 self.pretty(&mut nocolor);
1387 String::from_utf8(nocolor.into_inner()).expect("Warning printing produced invalid utf8")
1388 }
1389}
1390
1391fn pluralise(string: String, quantity: i64) -> String {
1392 if quantity == 1 {
1393 string
1394 } else {
1395 format!("{string}s")
1396 }
1397}