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