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