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