Fork of daniellemaywood.uk/gleam — Wasm codegen work
65 kB
1585 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 Target::Wasm => "wasm",
392 };
393
394 Diagnostic {
395 title: "Deprecated target shorthand syntax".into(),
396 text: wrap_format!(
397 "This shorthand target name is deprecated. \
398Use the full name: `{full_name}` instead."
399 ),
400 hint: None,
401 level: diagnostic::Level::Warning,
402 location: Some(Location {
403 label: diagnostic::Label {
404 text: Some(format!("This should be replaced with `{full_name}`")),
405 span: *location,
406 },
407 path: path.clone(),
408 src: src.clone(),
409 extra_labels: vec![],
410 }),
411 }
412 }
413
414 Warning::DetachedDocComment {
415 path,
416 src,
417 location,
418 } => Diagnostic {
419 title: "Detached doc comment".into(),
420 text: wrap(
421 "This doc comment is followed by a regular \
422comment so it is not attached to any definition.",
423 ),
424 level: diagnostic::Level::Warning,
425 location: Some(Location {
426 path: path.to_path_buf(),
427 src: src.clone(),
428 label: diagnostic::Label {
429 text: Some("This is not attached to a definition".into()),
430 span: *location,
431 },
432 extra_labels: Vec::new(),
433 }),
434 hint: Some("Move the comment above the doc comment".into()),
435 },
436
437 Warning::Type { path, warning, src } => match warning.as_ref() {
438 type_::Warning::Todo {
439 kind,
440 location,
441 type_,
442 names,
443 } => {
444 let (title, text) = match kind {
445 TodoKind::Keyword => {
446 let text = "\
447This code will crash if it is run. Be sure to finish it before
448running your program.";
449
450 ("Todo found", text)
451 }
452
453 TodoKind::EmptyBlock => {
454 let text = "\
455A block must always contain at least one expression.
456
457A todo expression has been used in place of the missing code,
458so this code will crash if it is run. Be sure to finish it before
459running your program.";
460 ("Incomplete block", text)
461 }
462 TodoKind::EmptyFunction { .. } => {
463 let text = "\
464A function must always have an implementation.
465
466A todo expression has been used in place of the missing body,
467so this code will crash if it is run. Be sure to finish it before
468running your program.";
469 ("Unimplemented function", text)
470 }
471 TodoKind::IncompleteUse => {
472 let text = "\
473A use expression must always be followed by at least one expression.
474
475A todo expression has been used in place of the missing code, so
476this code will crash if it is run. Be sure to finish it before
477running your program.";
478 ("Incomplete use expression", text)
479 }
480 };
481
482 let hint = if !type_.is_variable() {
483 Some(format!(
484 "I think its type is `{}`.\n",
485 Printer::new(names).print_type(type_)
486 ))
487 } else {
488 None
489 };
490
491 Diagnostic {
492 title: title.into(),
493 text: text.into(),
494 level: diagnostic::Level::Warning,
495 location: Some(Location {
496 path: path.to_path_buf(),
497 src: src.clone(),
498 label: diagnostic::Label {
499 text: Some("This code is incomplete".into()),
500 span: *location,
501 },
502 extra_labels: Vec::new(),
503 }),
504 hint,
505 }
506 }
507
508 type_::Warning::ImplicitlyDiscardedResult { location } => Diagnostic {
509 title: "Unused result value".into(),
510 text: "".into(),
511 hint: Some(
512 "If you are sure you don't need it you can assign it to `_`.".into(),
513 ),
514 level: diagnostic::Level::Warning,
515 location: Some(Location {
516 path: path.to_path_buf(),
517 src: src.clone(),
518 label: diagnostic::Label {
519 text: Some("The Result value created here is unused".into()),
520 span: *location,
521 },
522 extra_labels: Vec::new(),
523 }),
524 },
525
526 type_::Warning::UnusedLiteral { location } => Diagnostic {
527 title: "Unused literal".into(),
528 text: "".into(),
529 hint: Some("You can safely remove it.".into()),
530 level: diagnostic::Level::Warning,
531 location: Some(Location {
532 path: path.to_path_buf(),
533 src: src.clone(),
534 label: diagnostic::Label {
535 text: Some("This value is never used".into()),
536 span: *location,
537 },
538 extra_labels: Vec::new(),
539 }),
540 },
541
542 type_::Warning::NoFieldsRecordUpdate { location } => Diagnostic {
543 title: "Fieldless record update".into(),
544 text: "".into(),
545 hint: Some(
546 "Add some fields to change or replace it with the record itself.".into(),
547 ),
548 level: diagnostic::Level::Warning,
549 location: Some(Location {
550 path: path.to_path_buf(),
551 src: src.clone(),
552 label: diagnostic::Label {
553 text: Some("This record update doesn't change any fields".into()),
554 span: *location,
555 },
556 extra_labels: Vec::new(),
557 }),
558 },
559
560 type_::Warning::AllFieldsRecordUpdate { location, .. } => Diagnostic {
561 title: "Redundant record update".into(),
562 text: "".into(),
563 hint: Some("It is better style to use the record creation syntax.".into()),
564 level: diagnostic::Level::Warning,
565 location: Some(Location {
566 src: src.clone(),
567 path: path.to_path_buf(),
568 label: diagnostic::Label {
569 text: Some("This record update specifies all fields".into()),
570 span: *location,
571 },
572 extra_labels: Vec::new(),
573 }),
574 },
575
576 type_::Warning::UnusedType {
577 location, imported, ..
578 } => {
579 let title = if *imported {
580 "Unused imported type".into()
581 } else {
582 "Unused private type".into()
583 };
584 let label = if *imported {
585 "This imported type is never used".into()
586 } else {
587 "This private type is never used".into()
588 };
589 Diagnostic {
590 title,
591 text: "".into(),
592 hint: Some("You can safely remove it.".into()),
593 level: diagnostic::Level::Warning,
594 location: Some(Location {
595 src: src.clone(),
596 path: path.to_path_buf(),
597 label: diagnostic::Label {
598 text: Some(label),
599 span: *location,
600 },
601 extra_labels: Vec::new(),
602 }),
603 }
604 }
605
606 type_::Warning::UnusedConstructor {
607 location, imported, ..
608 } => {
609 let title = if *imported {
610 "Unused imported item".into()
611 } else {
612 "Unused private constructor".into()
613 };
614 let label = if *imported {
615 "This imported constructor is never used".into()
616 } else {
617 "This private constructor is never used".into()
618 };
619 Diagnostic {
620 title,
621 text: "".into(),
622 hint: Some("You can safely remove it.".into()),
623 level: diagnostic::Level::Warning,
624 location: Some(Location {
625 src: src.clone(),
626 path: path.to_path_buf(),
627 label: diagnostic::Label {
628 text: Some(label),
629 span: *location,
630 },
631 extra_labels: Vec::new(),
632 }),
633 }
634 }
635
636 type_::Warning::UnusedImportedModule { location, .. } => Diagnostic {
637 title: "Unused imported module".into(),
638 text: "".into(),
639 hint: Some("You can safely remove it.".into()),
640 level: diagnostic::Level::Warning,
641 location: Some(Location {
642 src: src.clone(),
643 path: path.to_path_buf(),
644 label: diagnostic::Label {
645 text: Some("This imported module is never used".into()),
646 span: *location,
647 },
648 extra_labels: Vec::new(),
649 }),
650 },
651
652 type_::Warning::UnusedImportedModuleAlias {
653 location,
654 module_name,
655 ..
656 } => {
657 let text = format!(
658 "\
659Hint: You can safely remove it.
660
661 import {module_name} as _
662"
663 );
664 Diagnostic {
665 title: "Unused imported module alias".into(),
666 text,
667 hint: None,
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 alias is never used".into()),
674 span: *location,
675 },
676 extra_labels: Vec::new(),
677 }),
678 }
679 }
680
681 type_::Warning::UnusedImportedValue { location, .. } => Diagnostic {
682 title: "Unused imported value".into(),
683 text: "".into(),
684 hint: Some("You can safely remove it.".into()),
685 level: diagnostic::Level::Warning,
686 location: Some(Location {
687 src: src.clone(),
688 path: path.to_path_buf(),
689 label: diagnostic::Label {
690 text: Some("This imported value is never used".into()),
691 span: *location,
692 },
693 extra_labels: Vec::new(),
694 }),
695 },
696
697 type_::Warning::UnusedPrivateModuleConstant { location, .. } => Diagnostic {
698 title: "Unused private constant".into(),
699 text: "".into(),
700 hint: Some("You can safely remove it.".into()),
701 level: diagnostic::Level::Warning,
702 location: Some(Location {
703 src: src.clone(),
704 path: path.to_path_buf(),
705 label: diagnostic::Label {
706 text: Some("This private constant is never used".into()),
707 span: *location,
708 },
709 extra_labels: Vec::new(),
710 }),
711 },
712
713 type_::Warning::UnusedPrivateFunction { location, .. } => Diagnostic {
714 title: "Unused private function".into(),
715 text: "".into(),
716 hint: Some("You can safely remove it.".into()),
717 level: diagnostic::Level::Warning,
718 location: Some(Location {
719 src: src.clone(),
720 path: path.to_path_buf(),
721 label: diagnostic::Label {
722 text: Some("This private function is never used".into()),
723 span: *location,
724 },
725 extra_labels: Vec::new(),
726 }),
727 },
728
729 type_::Warning::UnusedVariable { location, origin } => Diagnostic {
730 title: if origin.is_function_parameter() {
731 "Unused function argument".into()
732 } else {
733 "Unused variable".into()
734 },
735 text: "".into(),
736 hint: origin.how_to_ignore(),
737 level: diagnostic::Level::Warning,
738 location: Some(Location {
739 src: src.clone(),
740 path: path.to_path_buf(),
741 label: diagnostic::Label {
742 text: if origin.is_function_parameter() {
743 Some("This argument is never used".into())
744 } else {
745 Some("This variable is never used".into())
746 },
747 span: *location,
748 },
749 extra_labels: Vec::new(),
750 }),
751 },
752
753 type_::Warning::UnusedRecursiveArgument { location } => Diagnostic {
754 title: "Unused function argument".into(),
755 text: wrap(
756 "This argument is passed to the function when recursing, \
757but it's never used for anything.",
758 ),
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: Some("This argument is never used".into()),
766 span: *location,
767 },
768 extra_labels: vec![],
769 }),
770 },
771
772 type_::Warning::UnnecessaryDoubleIntNegation { location } => Diagnostic {
773 title: "Unnecessary double negation (--) on integer".into(),
774 text: "".into(),
775 hint: None,
776 level: diagnostic::Level::Warning,
777 location: Some(Location {
778 src: src.clone(),
779 path: path.to_path_buf(),
780 label: diagnostic::Label {
781 text: Some("You can safely remove this.".into()),
782 span: *location,
783 },
784 extra_labels: Vec::new(),
785 }),
786 },
787 type_::Warning::UnnecessaryDoubleBoolNegation { location } => Diagnostic {
788 title: "Unnecessary double negation (!!) on bool".into(),
789 text: "".into(),
790 hint: None,
791 level: diagnostic::Level::Warning,
792 location: Some(Location {
793 src: src.clone(),
794 path: path.to_path_buf(),
795 label: diagnostic::Label {
796 text: Some("You can safely remove this.".into()),
797 span: *location,
798 },
799 extra_labels: Vec::new(),
800 }),
801 },
802 type_::Warning::InefficientEmptyListCheck { location, kind } => {
803 use type_::error::EmptyListCheckKind;
804 let text = wrap(
805 "The `list.length` function has to iterate across the whole \
806list to calculate the length, which is wasteful if you only \
807need to know if the list is empty or not.",
808 );
809 let hint = Some(match kind {
810 EmptyListCheckKind::Empty => "You can use `the_list == []` instead.".into(),
811 EmptyListCheckKind::NonEmpty => {
812 "You can use `the_list != []` instead.".into()
813 }
814 });
815
816 Diagnostic {
817 title: "Inefficient use of `list.length`".into(),
818 text,
819 hint,
820 level: diagnostic::Level::Warning,
821 location: Some(Location {
822 src: src.clone(),
823 path: path.to_path_buf(),
824 label: diagnostic::Label {
825 text: None,
826 span: *location,
827 },
828 extra_labels: Vec::new(),
829 }),
830 }
831 }
832
833 type_::Warning::TransitiveDependencyImported {
834 location,
835 module,
836 package,
837 } => {
838 let text = wrap_format!(
839 "The module `{module}` is being imported, but \
840`{package}`, the package it belongs to, is not a direct dependency of your \
841package.
842In a future version of Gleam this may become a compile error.
843
844Run this command to add it to your dependencies:
845
846 gleam add {package}
847"
848 );
849 Diagnostic {
850 title: "Transitive dependency imported".into(),
851 text,
852 hint: None,
853 level: diagnostic::Level::Warning,
854 location: Some(Location {
855 src: src.clone(),
856 path: path.to_path_buf(),
857 label: diagnostic::Label {
858 text: None,
859 span: *location,
860 },
861 extra_labels: Vec::new(),
862 }),
863 }
864 }
865
866 type_::Warning::DeprecatedItem {
867 location,
868 message,
869 layer,
870 } => {
871 let text = wrap_format!("It was deprecated with this message: {message}");
872 let (title, diagnostic_label_text) = if layer.is_value() {
873 (
874 "Deprecated value used".into(),
875 Some("This value has been deprecated".into()),
876 )
877 } else {
878 (
879 "Deprecated type used".into(),
880 Some("This type has been deprecated".into()),
881 )
882 };
883
884 Diagnostic {
885 title,
886 text,
887 hint: None,
888 level: diagnostic::Level::Warning,
889 location: Some(Location {
890 src: src.clone(),
891 path: path.to_path_buf(),
892 label: diagnostic::Label {
893 text: diagnostic_label_text,
894 span: *location,
895 },
896 extra_labels: Vec::new(),
897 }),
898 }
899 }
900
901 type_::Warning::UnreachableCasePattern { location, reason } => {
902 let text = match reason {
903 UnreachablePatternReason::DuplicatePattern => wrap(
904 "This pattern cannot be reached as a previous \
905pattern matches the same values.",
906 ),
907 UnreachablePatternReason::ImpossibleVariant => wrap(
908 "This pattern cannot be reached as it matches on \
909a variant of a type which is never present.",
910 ),
911 UnreachablePatternReason::ImpossibleSegments(_) => wrap(
912 "This pattern cannot be reached as it contains \
913segments that will never match.",
914 ),
915 };
916
917 let extra_labels = match reason {
918 UnreachablePatternReason::DuplicatePattern
919 | UnreachablePatternReason::ImpossibleVariant => vec![],
920 UnreachablePatternReason::ImpossibleSegments(segments) => segments
921 .iter()
922 .map(|segment| ExtraLabel {
923 src_info: None,
924 label: diagnostic::Label {
925 text: Some(explain_impossible_segment(segment)),
926 span: segment.location(),
927 },
928 })
929 .collect_vec(),
930 };
931
932 Diagnostic {
933 title: "Unreachable pattern".into(),
934 text,
935 hint: Some("It can be safely removed.".into()),
936 level: diagnostic::Level::Warning,
937 location: Some(Location {
938 src: src.clone(),
939 path: path.to_path_buf(),
940 label: diagnostic::Label {
941 text: None,
942 span: *location,
943 },
944 extra_labels,
945 }),
946 }
947 }
948
949 type_::Warning::CaseMatchOnLiteralCollection { kind, location } => {
950 let kind = match kind {
951 LiteralCollectionKind::List => "list",
952 LiteralCollectionKind::Tuple => "tuple",
953 LiteralCollectionKind::Record => "record",
954 };
955
956 let title = format!("Redundant {kind}");
957 let text = wrap_format!(
958 "Instead of building a {kind} and matching on it, \
959you can match on its contents directly.
960A case expression can take multiple subjects separated by commas like this:
961
962 case one_subject, another_subject {{
963 _, _ -> todo
964 }}
965
966See: https://tour.gleam.run/flow-control/multiple-subjects/"
967 );
968
969 Diagnostic {
970 title,
971 text,
972 hint: None,
973 level: diagnostic::Level::Warning,
974 location: Some(Location {
975 src: src.clone(),
976 path: path.to_path_buf(),
977 label: diagnostic::Label {
978 text: Some(format!("You can remove this {kind} wrapper")),
979 span: *location,
980 },
981 extra_labels: Vec::new(),
982 }),
983 }
984 }
985
986 type_::Warning::CaseMatchOnLiteralValue { location } => Diagnostic {
987 title: "Match on a literal value".into(),
988 text: wrap(
989 "Matching on a literal value is redundant since you \
990can already tell which branch is going to match with this value.",
991 ),
992 hint: None,
993 level: diagnostic::Level::Warning,
994 location: Some(Location {
995 src: src.clone(),
996 path: path.to_path_buf(),
997 label: diagnostic::Label {
998 text: Some("There's no need to pattern match on this value".into()),
999 span: *location,
1000 },
1001 extra_labels: Vec::new(),
1002 }),
1003 },
1004
1005 type_::Warning::OpaqueExternalType { location } => Diagnostic {
1006 title: "Opaque external type".into(),
1007 text: "This type has no constructors so making it opaque is redundant.".into(),
1008 hint: Some("Remove the `opaque` qualifier from the type definition.".into()),
1009 level: diagnostic::Level::Warning,
1010 location: Some(Location {
1011 src: src.clone(),
1012 path: path.to_path_buf(),
1013 label: diagnostic::Label {
1014 text: None,
1015 span: *location,
1016 },
1017 extra_labels: Vec::new(),
1018 }),
1019 },
1020
1021 type_::Warning::UnusedValue { location } => Diagnostic {
1022 title: "Unused value".into(),
1023 text: wrap(
1024 "This expression computes a value without any side \
1025effects, but then the value isn't used at all. You might want to assign it to a \
1026variable, or delete the expression entirely if it's not needed.",
1027 ),
1028 hint: None,
1029 level: diagnostic::Level::Warning,
1030 location: Some(Location {
1031 path: path.to_path_buf(),
1032 src: src.clone(),
1033 label: diagnostic::Label {
1034 text: Some("This value is never used".into()),
1035 span: *location,
1036 },
1037 extra_labels: Vec::new(),
1038 }),
1039 },
1040
1041 type_::Warning::RedundantAssertAssignment { location } => Diagnostic {
1042 title: "Redundant assertion".into(),
1043 text: wrap(
1044 "This assertion is redundant since the pattern covers all possibilities.",
1045 ),
1046 hint: None,
1047 level: diagnostic::Level::Warning,
1048 location: Some(Location {
1049 label: diagnostic::Label {
1050 text: Some("You can remove this".into()),
1051 span: *location,
1052 },
1053 path: path.clone(),
1054 src: src.clone(),
1055 extra_labels: vec![],
1056 }),
1057 },
1058
1059 type_::Warning::AssertAssignmentOnImpossiblePattern { location, reason } => {
1060 let extra_labels = match reason {
1061 AssertImpossiblePattern::InferredVariant => vec![],
1062 AssertImpossiblePattern::ImpossibleSegments { segments } => segments
1063 .iter()
1064 .map(|segment| ExtraLabel {
1065 src_info: None,
1066 label: diagnostic::Label {
1067 text: Some(explain_impossible_segment(segment)),
1068 span: segment.location(),
1069 },
1070 })
1071 .collect_vec(),
1072 };
1073
1074 Diagnostic {
1075 title: "Assertion that will always fail".into(),
1076 text: wrap(
1077 "We can tell from the code above that the value will never match \
1078this pattern and that this code will always crash.
1079
1080Either change the pattern or use `panic` to unconditionally fail.",
1081 ),
1082 hint: None,
1083 level: diagnostic::Level::Warning,
1084 location: Some(Location {
1085 label: diagnostic::Label {
1086 text: None,
1087 span: *location,
1088 },
1089 path: path.clone(),
1090 src: src.clone(),
1091 extra_labels,
1092 }),
1093 }
1094 }
1095
1096 type_::Warning::TodoOrPanicUsedAsFunction {
1097 kind,
1098 location,
1099 arguments_location,
1100 arguments,
1101 } => {
1102 let title = match kind {
1103 TodoOrPanic::Todo => "Todo used as a function".into(),
1104 TodoOrPanic::Panic => "Panic used as a function".into(),
1105 };
1106 let label_location = match arguments_location {
1107 None => location,
1108 Some(location) => location,
1109 };
1110 let name = match kind {
1111 TodoOrPanic::Todo => "todo",
1112 TodoOrPanic::Panic => "panic",
1113 };
1114 let mut text = format!("`{name}` is not a function");
1115 match arguments {
1116 0 => {
1117 text.push_str(", you can just write `");
1118 text.push_str(name);
1119 text.push_str("` instead of `");
1120 text.push_str(name);
1121 text.push_str("()`.");
1122 }
1123 1 => text.push_str(
1124 " and will crash before it can do anything with this argument.",
1125 ),
1126 _ => text.push_str(
1127 " and will crash before it can do anything with these arguments.",
1128 ),
1129 }
1130
1131 let hint = match arguments {
1132 0 => None,
1133 _ => Some(wrap_format!(
1134 "if you want to display an error message you should write
1135`{name} as \"my error message\"`
1136See: https://tour.gleam.run/advanced-features/{name}/"
1137 )),
1138 };
1139
1140 Diagnostic {
1141 title,
1142 text: wrap(&text),
1143 hint,
1144 level: diagnostic::Level::Warning,
1145 location: Some(Location {
1146 label: diagnostic::Label {
1147 text: None,
1148 span: *label_location,
1149 },
1150 path: path.clone(),
1151 src: src.clone(),
1152 extra_labels: vec![],
1153 }),
1154 }
1155 }
1156
1157 type_::Warning::UnreachableCodeAfterPanic {
1158 location,
1159 panic_position: unreachable_code_kind,
1160 } => {
1161 let text = match unreachable_code_kind {
1162 PanicPosition::PreviousExpression => {
1163 "This code is unreachable because it comes after a `panic`."
1164 }
1165 PanicPosition::PreviousFunctionArgument => {
1166 "This argument is unreachable because the previous one always panics. \
1167Your code will crash before reaching this point."
1168 }
1169 PanicPosition::LastFunctionArgument => {
1170 "This function call is unreachable because its last argument always panics. \
1171Your code will crash before reaching this point."
1172 }
1173 PanicPosition::EchoExpression => {
1174 "This `echo` won't print anything because the expression it \
1175should be printing always panics."
1176 }
1177 };
1178
1179 Diagnostic {
1180 title: "Unreachable code".into(),
1181 text: wrap(text),
1182 hint: None,
1183 level: diagnostic::Level::Warning,
1184 location: Some(Location {
1185 label: diagnostic::Label {
1186 text: None,
1187 span: *location,
1188 },
1189 path: path.clone(),
1190 src: src.clone(),
1191 extra_labels: vec![],
1192 }),
1193 }
1194 }
1195
1196 type_::Warning::RedundantPipeFunctionCapture { location } => Diagnostic {
1197 title: "Redundant function capture".into(),
1198 text: wrap(
1199 "This function capture is redundant since the value is \
1200already piped as the first argument of this call.
1201
1202See: https://tour.gleam.run/functions/pipelines/",
1203 ),
1204 hint: None,
1205 level: diagnostic::Level::Warning,
1206 location: Some(Location {
1207 label: diagnostic::Label {
1208 text: Some("You can safely remove this".into()),
1209 span: *location,
1210 },
1211 path: path.clone(),
1212 src: src.clone(),
1213 extra_labels: vec![],
1214 }),
1215 },
1216 type_::Warning::FeatureRequiresHigherGleamVersion {
1217 location,
1218 minimum_required_version,
1219 wrongfully_allowed_version,
1220 feature_kind,
1221 } => {
1222 let feature = match feature_kind {
1223 FeatureKind::LabelShorthandSyntax => "The label shorthand syntax was",
1224 FeatureKind::ConstantStringConcatenation => {
1225 "Constant strings concatenation was"
1226 }
1227 FeatureKind::ArithmeticInGuards => "Arithmetic operations in guards were",
1228 FeatureKind::ConcatenateInGuards => "String concatenation in guards was",
1229 FeatureKind::UnannotatedUtf8StringSegment => {
1230 "The ability to omit the `utf8` annotation for string segments was"
1231 }
1232 FeatureKind::UnannotatedFloatSegment => {
1233 "The ability to omit the `float` annotation for float segments was"
1234 }
1235 FeatureKind::NestedTupleAccess => {
1236 "The ability to access nested tuple fields was"
1237 }
1238 FeatureKind::InternalAnnotation => "The `@internal` annotation was",
1239 FeatureKind::AtInJavascriptModules => {
1240 "The ability to have `@` in a Javascript module's name was"
1241 }
1242 FeatureKind::RecordUpdateVariantInference => {
1243 "Record updates for custom types when the variant is known was"
1244 }
1245 FeatureKind::RecordAccessVariantInference => {
1246 "Field access on custom types when the variant is known was"
1247 }
1248 FeatureKind::LetAssertWithMessage => {
1249 "Specifying a custom panic message when using let assert was"
1250 }
1251 FeatureKind::VariantWithDeprecatedAnnotation => {
1252 "Deprecating individual custom type variants was"
1253 }
1254 FeatureKind::JavaScriptUnalignedBitArray => {
1255 "Use of unaligned bit arrays on the JavaScript target was"
1256 }
1257 FeatureKind::BoolAssert => "The bool `assert` statement was",
1258 FeatureKind::ExpressionInSegmentSize => "Expressions in segment sizes were",
1259 FeatureKind::ExternalCustomType => {
1260 "The `@external` annotation on custom types was"
1261 }
1262 FeatureKind::ConstantRecordUpdate => {
1263 "The record update syntax for constants was"
1264 }
1265 FeatureKind::ConstantListWithTail => "Prepending to a constant list was",
1266 };
1267
1268 Diagnostic {
1269 title: "Incompatible gleam version range".into(),
1270 text: wrap_format!(
1271 "{feature} introduced in version v{minimum_required_version}. But the Gleam version range \
1272 specified in your `gleam.toml` would allow this code to run on an earlier \
1273 version like v{wrongfully_allowed_version}, resulting in compilation errors!",
1274 ),
1275 hint: Some(wrap_format!(
1276 "Remove the version constraint from your `gleam.toml` or update it to be:
1277
1278 gleam = \">= {minimum_required_version}\""
1279 )),
1280 level: diagnostic::Level::Warning,
1281 location: Some(Location {
1282 label: diagnostic::Label {
1283 text: Some(format!(
1284 "This requires a Gleam version >= {minimum_required_version}"
1285 )),
1286 span: *location,
1287 },
1288 path: path.clone(),
1289 src: src.clone(),
1290 extra_labels: vec![],
1291 }),
1292 }
1293 }
1294
1295 type_::Warning::JavaScriptIntUnsafe { location } => Diagnostic {
1296 title: "Int is outside JavaScript's safe integer range".into(),
1297 text: wrap(
1298 "This integer value is too large to be represented accurately by \
1299JavaScript's number type. To avoid this warning integer values must be in the range \
1300-(2^53 - 1) - (2^53 - 1).
1301
1302See JavaScript's Number.MAX_SAFE_INTEGER and Number.MIN_SAFE_INTEGER properties for more \
1303information.",
1304 ),
1305 hint: None,
1306 level: diagnostic::Level::Warning,
1307 location: Some(Location {
1308 path: path.to_path_buf(),
1309 src: src.clone(),
1310 label: diagnostic::Label {
1311 text: Some("This is not a safe integer value on JavaScript".into()),
1312 span: *location,
1313 },
1314 extra_labels: Vec::new(),
1315 }),
1316 },
1317
1318 type_::Warning::BitArraySegmentTruncatedValue {
1319 location: _,
1320 truncation:
1321 BitArraySegmentTruncation {
1322 truncated_value,
1323 truncated_into,
1324 segment_bits,
1325 value_location,
1326 },
1327 } => {
1328 let (unit, segment_size, taken) = if segment_bits % 8 == 0 {
1329 let bytes = segment_bits / 8;
1330 let segment_size = pluralise(format!("{bytes} byte"), bytes);
1331 let taken = if bytes == 1 {
1332 "first byte".into()
1333 } else {
1334 format!("first {bytes} bytes")
1335 };
1336
1337 ("bytes", segment_size, taken)
1338 } else {
1339 let segment_size = pluralise(format!("{segment_bits} bit"), *segment_bits);
1340 let taken = if *segment_bits == 1 {
1341 "first bit".into()
1342 } else {
1343 format!("first {segment_bits} bits")
1344 };
1345 ("bits", segment_size, taken)
1346 };
1347
1348 let text = wrap_format!(
1349 "This segment is {segment_size} long, but {truncated_value} \
1350doesn't fit in that many {unit}. It would be truncated by taking its {taken}, \
1351resulting in the value {truncated_into}."
1352 );
1353
1354 Diagnostic {
1355 title: "Truncated bit array segment".into(),
1356 text,
1357 hint: None,
1358 level: diagnostic::Level::Warning,
1359 location: Some(Location {
1360 path: path.to_path_buf(),
1361 src: src.clone(),
1362 label: diagnostic::Label {
1363 text: Some(format!(
1364 "You can safely replace this with {truncated_into}"
1365 )),
1366 span: *value_location,
1367 },
1368 extra_labels: vec![],
1369 }),
1370 }
1371 }
1372
1373 type_::Warning::JavaScriptBitArrayUnsafeInt { location, size } => {
1374 let text = wrap_format!(
1375 "This segment is a {size}-bit long int, but on the \
1376JavaScript target numbers have at most 52 bits. It would be truncated to its \
1377first 52 bits."
1378 );
1379
1380 Diagnostic {
1381 title: "Truncated bit array segment".into(),
1382 text: wrap(&text),
1383 hint: Some("Did you mean to use the `bytes` segment option?".into()),
1384 level: diagnostic::Level::Warning,
1385 location: Some(Location {
1386 path: path.to_path_buf(),
1387 src: src.clone(),
1388 label: diagnostic::Label {
1389 text: None,
1390 span: *location,
1391 },
1392 extra_labels: vec![],
1393 }),
1394 }
1395 }
1396
1397 type_::Warning::AssertLiteralBool { location } => Diagnostic {
1398 title: "Assertion of a literal value".into(),
1399 text: wrap(
1400 "Asserting on a literal bool is redundant since you \
1401can already tell whether it will be `True` or `False`.",
1402 ),
1403 hint: None,
1404 level: diagnostic::Level::Warning,
1405 location: Some(Location {
1406 src: src.clone(),
1407 path: path.to_path_buf(),
1408 label: diagnostic::Label {
1409 text: None,
1410 span: *location,
1411 },
1412 extra_labels: Vec::new(),
1413 }),
1414 },
1415
1416 type_::Warning::ModuleImportedTwice {
1417 name,
1418 first,
1419 second,
1420 } => Diagnostic {
1421 title: "Duplicate import".into(),
1422 text: format!("The {name} module has been imported twice."),
1423 hint: None,
1424 level: diagnostic::Level::Warning,
1425 location: Some(Location {
1426 src: src.clone(),
1427 path: path.to_path_buf(),
1428 label: diagnostic::Label {
1429 text: Some("Reimported here".into()),
1430 span: *second,
1431 },
1432 extra_labels: vec![ExtraLabel {
1433 src_info: None,
1434 label: diagnostic::Label {
1435 text: Some("First imported here".into()),
1436 span: *first,
1437 },
1438 }],
1439 }),
1440 },
1441
1442 type_::Warning::UnusedDiscardPattern { location, name } => Diagnostic {
1443 title: "Unused discard pattern".into(),
1444 text: format!("`_ as {name}` can be written more concisely as `{name}`"),
1445 level: diagnostic::Level::Warning,
1446 location: Some(Location {
1447 src: src.clone(),
1448 path: path.to_path_buf(),
1449 label: diagnostic::Label {
1450 text: None,
1451 span: SrcSpan {
1452 start: location.start - "_ as ".len() as u32,
1453 end: location.end,
1454 },
1455 },
1456 extra_labels: vec![],
1457 }),
1458 hint: None,
1459 },
1460
1461 type_::Warning::TopLevelDefinitionShadowsImport { location, name } => {
1462 let text = wrap_format!(
1463 "Definition of {name} shadows an imported value.
1464The imported value could not be used in this module anyway."
1465 );
1466 Diagnostic {
1467 title: "Shadowed Import".into(),
1468 text,
1469 level: diagnostic::Level::Warning,
1470 location: Some(Location {
1471 path: path.clone(),
1472 src: src.clone(),
1473 label: diagnostic::Label {
1474 text: Some(wrap_format!("`{name}` is defined here")),
1475 span: *location,
1476 },
1477 extra_labels: Vec::new(),
1478 }),
1479 hint: Some("Either rename the definition or remove the import.".into()),
1480 }
1481 }
1482
1483 type_::Warning::RedundantComparison { location, outcome } => Diagnostic {
1484 title: "Redundant comparison".into(),
1485 text: format!(
1486 "This comparison is redundant since it always {}.",
1487 match outcome {
1488 ComparisonOutcome::AlwaysSucceeds => "succeeds",
1489 ComparisonOutcome::AlwaysFails => "fails",
1490 }
1491 ),
1492 hint: None,
1493 level: diagnostic::Level::Warning,
1494 location: Some(Location {
1495 label: diagnostic::Label {
1496 text: Some(format!(
1497 "This is always `{}`",
1498 match outcome {
1499 ComparisonOutcome::AlwaysSucceeds => "True",
1500 ComparisonOutcome::AlwaysFails => "False",
1501 }
1502 )),
1503 span: *location,
1504 },
1505 path: path.clone(),
1506 src: src.clone(),
1507 extra_labels: vec![],
1508 }),
1509 },
1510
1511 type_::Warning::PipeIntoCallWhichReturnsFunction { location } => Diagnostic {
1512 title: "Deprecated pipe use".into(),
1513 text: wrap(
1514 "This use of pipelines is deprecated, as it creates \
1515ambiguity in pipe syntax. Currently `a |> b(c)` can desugar to either `b(a, c)` or \
1516`b(a)(c)` depending on the type of `b`. The latter syntax is deprecated, and should \
1517not be used.",
1518 ),
1519 hint: Some(
1520 "Add an extra set of parentheses after the call, like `a |> b(c)()`".into(),
1521 ),
1522 level: diagnostic::Level::Warning,
1523 location: Some(Location {
1524 label: diagnostic::Label {
1525 text: Some("This should have an extra set of parentheses".into()),
1526 span: *location,
1527 },
1528 path: path.clone(),
1529 src: src.clone(),
1530 extra_labels: vec![],
1531 }),
1532 },
1533 },
1534
1535 Warning::EmptyModule { path: _, name } => Diagnostic {
1536 title: "Empty module".into(),
1537 text: format!("Module '{name}' contains no public definitions."),
1538 hint: Some("You can safely remove this module.".into()),
1539 level: diagnostic::Level::Warning,
1540 location: None,
1541 },
1542 }
1543 }
1544
1545 pub fn pretty(&self, buffer: &mut Buffer) {
1546 self.to_diagnostic().write(buffer);
1547 buffer
1548 .write_all(b"\n")
1549 .expect("error pretty buffer write space after");
1550 }
1551
1552 pub fn to_pretty_string(&self) -> String {
1553 let mut nocolor = Buffer::no_color();
1554 self.pretty(&mut nocolor);
1555 String::from_utf8(nocolor.into_inner()).expect("Warning printing produced invalid utf8")
1556 }
1557}
1558
1559fn explain_impossible_segment(segment: &ImpossibleBitArraySegmentPattern) -> String {
1560 match segment {
1561 ImpossibleBitArraySegmentPattern::UnrepresentableInteger {
1562 size,
1563 signed,
1564 value: _,
1565 location: _,
1566 } => {
1567 let human_readable_size = match size {
1568 1 => "1 bit".into(),
1569 8 => "1 byte".into(),
1570 n if n % 8 == 0 => format!("{} bytes", n / 8),
1571 n => format!("{n} bits"),
1572 };
1573 let sign = if *signed { "signed" } else { "unsigned" };
1574 format!("A {human_readable_size} {sign} integer will never match this value")
1575 }
1576 }
1577}
1578
1579fn pluralise(string: String, quantity: i64) -> String {
1580 if quantity == 1 {
1581 string
1582 } else {
1583 format!("{string}s")
1584 }
1585}