Skip to content

Commit 3e892ac

Browse files
giacomocavalierilpil
authored andcommitted
raise warning for function arguments used only in recursive calls
1 parent 0ba901e commit 3e892ac

15 files changed

+470
-34
lines changed

CHANGELOG.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,39 @@
6363

6464
([Giacomo Cavalieri](https://github.com/giacomocavalieri))
6565

66+
- The compiler now raises a warning when a function's argument is only passed
67+
along in a recursive call but not actually used for anything. For example:
68+
69+
```gleam
70+
import gleam/io
71+
72+
pub fn greet(x, times) {
73+
case times {
74+
0 -> Nil
75+
_ -> {
76+
io.println("Hello, Joe!")
77+
greet(x, times - 1)
78+
}
79+
}
80+
}
81+
```
82+
83+
In this piece of code the `x` argument is actually never used, and the
84+
compiler will raise the following warning:
85+
86+
```text
87+
warning: Unused function argument
88+
┌─ /Users/giacomocavalieri/Desktop/prova/src/prova.gleam:3:14
89+
90+
3 │ pub fn greet(x, times) {
91+
│ ^ This argument is unused
92+
93+
This argument is passed to the function when recursing, but it's never used
94+
for anything.
95+
```
96+
97+
([Giacomo Cavalieri](https://github.com/giacomocavalieri))
98+
6699
- The compiler now emits a better error message for private types marked as
67100
opaque. For example, the following piece of code:
68101

compiler-core/src/analyse.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,7 @@ impl<'a, A> ModuleAnalyzer<'a, A> {
599599
.expect("Could not find hydrator for fn");
600600

601601
let (arguments, body) = expr_typer.infer_fn_with_known_types(
602+
Some(name.clone()),
602603
typed_arguments.clone(),
603604
body,
604605
Some(prereg_return_type.clone()),

compiler-core/src/ast/typed.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,6 +1033,13 @@ impl TypedExpr {
10331033
}
10341034
}
10351035

1036+
pub fn var_constructor(&self) -> Option<&ValueConstructor> {
1037+
match self {
1038+
TypedExpr::Var { constructor, .. } => Some(constructor),
1039+
_ => None,
1040+
}
1041+
}
1042+
10361043
#[must_use]
10371044
pub(crate) fn is_panic(&self) -> bool {
10381045
match self {

compiler-core/src/language_server/code_action.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6236,7 +6236,7 @@ impl<'ast> ast::visit::Visit<'ast> for InlineVariable<'ast> {
62366236
VariableDeclaration::LetPattern => {}
62376237
VariableDeclaration::UsePattern
62386238
| VariableDeclaration::ClausePattern
6239-
| VariableDeclaration::FunctionParameter
6239+
| VariableDeclaration::FunctionParameter { .. }
62406240
| VariableDeclaration::Generated => return,
62416241
}
62426242

@@ -6256,7 +6256,7 @@ impl<'ast> ast::visit::Visit<'ast> for InlineVariable<'ast> {
62566256
VariableDeclaration::LetPattern => {}
62576257
VariableDeclaration::UsePattern
62586258
| VariableDeclaration::ClausePattern
6259-
| VariableDeclaration::FunctionParameter
6259+
| VariableDeclaration::FunctionParameter { .. }
62606260
| VariableDeclaration::Generated => return,
62616261
}
62626262

compiler-core/src/type_.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -877,6 +877,49 @@ impl ValueConstructorVariant {
877877
ValueConstructorVariant::Record { field_map, .. } => field_map.as_ref(),
878878
}
879879
}
880+
881+
/// If this is the constructor of a module function, returns the function's
882+
/// module and name.
883+
///
884+
fn function_module_and_name(&self) -> Option<(EcoString, EcoString)> {
885+
match self {
886+
ValueConstructorVariant::ModuleFn { name, module, .. } => {
887+
Some((module.clone(), name.clone()))
888+
}
889+
890+
ValueConstructorVariant::ModuleConstant { .. }
891+
| ValueConstructorVariant::LocalConstant { .. }
892+
| ValueConstructorVariant::LocalVariable { .. }
893+
| ValueConstructorVariant::Record { .. } => None,
894+
}
895+
}
896+
897+
/// If this is a constructor for an argument, returns the name of the
898+
/// function where it is defined and its index in the function's parameter
899+
/// list.
900+
///
901+
fn argument_function_and_index(&self) -> Option<(EcoString, usize)> {
902+
match self {
903+
ValueConstructorVariant::LocalVariable {
904+
location: _,
905+
origin:
906+
VariableOrigin {
907+
syntax: _,
908+
declaration:
909+
VariableDeclaration::FunctionParameter {
910+
function_name,
911+
index,
912+
},
913+
},
914+
} => Some((function_name.clone()?, *index)),
915+
916+
ValueConstructorVariant::LocalVariable { .. }
917+
| ValueConstructorVariant::ModuleConstant { .. }
918+
| ValueConstructorVariant::LocalConstant { .. }
919+
| ValueConstructorVariant::ModuleFn { .. }
920+
| ValueConstructorVariant::Record { .. } => None,
921+
}
922+
}
880923
}
881924

882925
#[derive(Debug, Clone, PartialEq, Eq)]

compiler-core/src/type_/environment.rs

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,17 @@ pub struct Environment<'a> {
7878
/// local_variable_usages is a stack of scopes. When a local variable is created it is
7979
/// added to the top scope. When a local variable is used we crawl down the scope
8080
/// stack for a variable with that name and mark it as used.
81-
/// NOTE: The bool in the tuple here tracks if the variable has been used
82-
pub local_variable_usages: Vec<HashMap<EcoString, (VariableOrigin, SrcSpan, bool)>>,
81+
///
82+
/// NOTE: The usize in the tuple here tracks the number of times the variable has
83+
/// been used.
84+
pub local_variable_usages: Vec<HashMap<EcoString, (VariableOrigin, SrcSpan, usize)>>,
85+
86+
/// Used to determine if an argument is only used unchanged in recursive
87+
/// calls, and could thus be removed.
88+
/// For each of the function's arguments keeps track of the usages in
89+
/// recursive calls.
90+
///
91+
pub recursive_argument_usages: HashMap<SrcSpan, Vec<SrcSpan>>,
8392

8493
/// Used to determine if all functions/constants need to support the current
8594
/// compilation target.
@@ -134,6 +143,7 @@ impl<'a> Environment<'a> {
134143
importable_modules,
135144
current_module,
136145
local_variable_usages: vec![HashMap::new()],
146+
recursive_argument_usages: HashMap::new(),
137147
target_support,
138148
names,
139149
module_type_aliases: HashMap::new(),
@@ -658,15 +668,15 @@ impl Environment<'_> {
658668
location: SrcSpan,
659669
problems: &mut Problems,
660670
) {
661-
if let Some((origin, location, false)) = self
671+
if let Some((origin, location, 0)) = self
662672
.local_variable_usages
663673
.last_mut()
664674
.expect("Attempted to access non-existent entity usages scope")
665-
.insert(name.clone(), (origin, location, false))
675+
.insert(name.clone(), (origin, location, 0))
666676
{
667677
// an entity was overwritten in the top most scope without being used
668678
let mut unused = HashMap::with_capacity(1);
669-
let _ = unused.insert(name, (origin, location, false));
679+
let _ = unused.insert(name, (origin, location, 0));
670680
self.handle_unused_variables(unused, problems);
671681
}
672682
}
@@ -681,7 +691,7 @@ impl Environment<'_> {
681691
.rev()
682692
.find_map(|scope| scope.get_mut(&name))
683693
{
684-
*used = true;
694+
*used += 1;
685695
}
686696
}
687697

@@ -795,13 +805,25 @@ impl Environment<'_> {
795805

796806
fn handle_unused_variables(
797807
&mut self,
798-
unused: HashMap<EcoString, (VariableOrigin, SrcSpan, bool)>,
808+
unused: HashMap<EcoString, (VariableOrigin, SrcSpan, usize)>,
799809
problems: &mut Problems,
800810
) {
801-
for (origin, location, used) in unused.into_values() {
802-
if !used {
811+
for (origin, location, usages) in unused.into_values() {
812+
if usages == 0 {
803813
problems.warning(Warning::UnusedVariable { location, origin });
804814
}
815+
// If the function parameter is actually used somewhere, but all the
816+
// usages are just passing it along in a recursive call, then it
817+
// counts as being unused too.
818+
else if origin.is_function_parameter()
819+
&& let Some(recursive_usages) = self.recursive_argument_usages.get(&location)
820+
&& recursive_usages.len() == usages
821+
{
822+
problems.warning(Warning::UnusedRecursiveArgument {
823+
location,
824+
usages: recursive_usages.clone(),
825+
});
826+
}
805827
}
806828
}
807829

compiler-core/src/type_/error.rs

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -761,13 +761,21 @@ pub enum VariableSyntax {
761761
Generated,
762762
}
763763

764-
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
764+
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
765765
/// The source of a variable, such as a `let` assignment, or function parameter.
766766
pub enum VariableDeclaration {
767767
LetPattern,
768768
UsePattern,
769769
ClausePattern,
770-
FunctionParameter,
770+
FunctionParameter {
771+
/// The name of the function defining the parameter. This will be None
772+
/// for parameters introduced by anoynmous functions: `fn(a) { a }`
773+
///
774+
function_name: Option<EcoString>,
775+
/// The index of the parameter in the function's parameter list.
776+
///
777+
index: usize,
778+
},
771779
Generated,
772780
}
773781

@@ -791,6 +799,17 @@ impl VariableOrigin {
791799
declaration: VariableDeclaration::Generated,
792800
}
793801
}
802+
803+
pub fn is_function_parameter(&self) -> bool {
804+
match self.declaration {
805+
VariableDeclaration::FunctionParameter { .. } => true,
806+
807+
VariableDeclaration::LetPattern
808+
| VariableDeclaration::UsePattern
809+
| VariableDeclaration::ClausePattern
810+
| VariableDeclaration::Generated => false,
811+
}
812+
}
794813
}
795814

796815
#[derive(Debug, Eq, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
@@ -1075,6 +1094,24 @@ pub enum Warning {
10751094
location: SrcSpan,
10761095
outcome: ComparisonOutcome,
10771096
},
1097+
/// When a function's argument is only ever used unchanged in recursive
1098+
/// calls. For example:
1099+
///
1100+
/// ```gleam
1101+
/// pub fn wibble(x, n) {
1102+
/// // ^ This argument is not needed,
1103+
/// case n {
1104+
/// 0 -> Nil
1105+
/// _ -> wibble(x, n - 1)
1106+
/// // ^ It's only used in recursive calls!
1107+
/// }
1108+
/// }
1109+
/// ```
1110+
///
1111+
UnusedRecursiveArgument {
1112+
location: SrcSpan,
1113+
usages: Vec<SrcSpan>,
1114+
},
10781115
}
10791116

10801117
#[derive(Debug, Eq, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
@@ -1333,7 +1370,8 @@ impl Warning {
13331370
| Warning::ModuleImportedTwice {
13341371
second: location, ..
13351372
}
1336-
| Warning::RedundantComparison { location, .. } => *location,
1373+
| Warning::RedundantComparison { location, .. }
1374+
| Warning::UnusedRecursiveArgument { location, .. } => *location,
13371375
}
13381376
}
13391377

0 commit comments

Comments
 (0)