Skip to content

Commit a228568

Browse files
giacomocavalierilpil
authored andcommitted
make sure renaming a variable in an alternative pattern renames all occurrences
1 parent da995eb commit a228568

10 files changed

+441
-9
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@
5959

6060
### Bug fixes
6161

62+
- Fixed a bug where renaming a variable from an alternative pattern would not
63+
rename all its occurrences.
64+
([Giacomo Cavalieri](https://github.com/giacomocavalieri))
65+
6266
- Fixed a typo in the error message emitted when trying to run a module that
6367
does not have a main function.
6468
([Louis Pilfold](https://github.com/lpil))

compiler-core/src/language_server/reference.rs

Lines changed: 149 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -366,21 +366,128 @@ pub struct FindVariableReferences {
366366
// To avoid this, we use a `HashSet` instead of a `Vec` here.
367367
// See: https://github.com/gleam-lang/gleam/issues/4859 and the linked PR.
368368
references: HashSet<VariableReference>,
369-
definition_location: SrcSpan,
369+
definition_location: DefinitionLocation,
370370
alternative_variable: AlternativeVariable,
371371
name: EcoString,
372372
}
373373

374+
/// Where the variable we're finding references for is defined.
375+
///
376+
enum DefinitionLocation {
377+
/// This is the location where the variable is defined, nothing special is
378+
/// going on here. For example:
379+
///
380+
/// ```gleam
381+
/// let wibble = 1
382+
/// // ^^^^^^ Definition location for `wibble`
383+
/// wibble + 1
384+
/// ^^^^^^
385+
/// // `wibble` used here, defined earlier
386+
/// ```
387+
///
388+
Regular { location: SrcSpan },
389+
390+
/// When dealing with alternative patterns and aliases we need special care:
391+
/// each usage wil always reference the first alternative where a variable
392+
/// is defined and not the following ones. For example:
393+
///
394+
/// ```gleam
395+
/// case wibble {
396+
/// [] as var | [_] as var -> var
397+
/// // ^^^ ^^^ If we look where `var` thinks it's defined
398+
/// // It will say it's defined here!
399+
/// }
400+
/// ```
401+
///
402+
/// This poses a problem if we start the renaming from the second
403+
/// alternative pattern:
404+
///
405+
/// ```gleam
406+
/// case wibble {
407+
/// [] as var | [_] as var -> var
408+
/// // ^^^ Since `var` uses the first alternative as its
409+
/// // definition location, this would not be considered
410+
/// // a reference to that same var.
411+
/// }
412+
/// ```
413+
///
414+
/// So we keep track of the location of this definition, but we also need
415+
/// to store the location of the first definition in the alternative case
416+
/// (that's `first_alternative_location`), so that when we look for
417+
/// references we can check against this one that is canonically used by
418+
/// expressions in the AST
419+
///
420+
Alternative {
421+
location: SrcSpan,
422+
first_alternative_location: SrcSpan,
423+
},
424+
}
425+
374426
impl FindVariableReferences {
375427
pub fn new(variable_definition_location: SrcSpan, variable_name: EcoString) -> Self {
376428
Self {
377429
references: HashSet::new(),
378-
definition_location: variable_definition_location,
430+
definition_location: DefinitionLocation::Regular {
431+
location: variable_definition_location,
432+
},
379433
alternative_variable: AlternativeVariable::Ignore,
380434
name: variable_name,
381435
}
382436
}
383437

438+
/// Where the definition for which we're accumulating references is
439+
/// originally defined. In case of alternative patterns this will point to
440+
/// the first occurrence of that name! Look at the docs for
441+
/// `DefinitionLocation` to learn more on why this is needed.
442+
///
443+
fn definition_origin_location(&self) -> SrcSpan {
444+
match self.definition_location {
445+
DefinitionLocation::Regular { location }
446+
| DefinitionLocation::Alternative {
447+
first_alternative_location: location,
448+
..
449+
} => location,
450+
}
451+
}
452+
453+
/// This is the location of the definition for which we're accumulating
454+
/// references. In most cases you'll want to use `definition_origin_location`.
455+
/// The difference between the two is explained in greater detail in the docs
456+
/// for `DefinitionLocation`.
457+
///
458+
fn definition_location(&self) -> SrcSpan {
459+
match self.definition_location {
460+
DefinitionLocation::Regular { location }
461+
| DefinitionLocation::Alternative { location, .. } => location,
462+
}
463+
}
464+
465+
fn update_alternative_origin(&mut self, alternative_location: SrcSpan) {
466+
match self.definition_location {
467+
// We've found the location of the origin of an alternative pattern.
468+
DefinitionLocation::Regular { location } if alternative_location < location => {
469+
self.definition_location = DefinitionLocation::Alternative {
470+
location,
471+
first_alternative_location: alternative_location,
472+
};
473+
}
474+
475+
// Since the new alternative location we've found is smaller, that
476+
// is the actual first one for the alternative pattern!
477+
DefinitionLocation::Alternative {
478+
location,
479+
first_alternative_location,
480+
} if alternative_location < first_alternative_location => {
481+
self.definition_location = DefinitionLocation::Alternative {
482+
location,
483+
first_alternative_location: alternative_location,
484+
};
485+
}
486+
487+
_ => (),
488+
};
489+
}
490+
384491
pub fn find_in_module(mut self, module: &TypedModule) -> HashSet<VariableReference> {
385492
self.visit_typed_module(module);
386493
self.references
@@ -394,7 +501,10 @@ impl FindVariableReferences {
394501

395502
impl<'ast> Visit<'ast> for FindVariableReferences {
396503
fn visit_typed_function(&mut self, fun: &'ast ast::TypedFunction) {
397-
if fun.full_location().contains(self.definition_location.start) {
504+
if fun
505+
.full_location()
506+
.contains(self.definition_origin_location().start)
507+
{
398508
ast::visit::visit_typed_function(self, fun);
399509
}
400510
}
@@ -409,7 +519,7 @@ impl<'ast> Visit<'ast> for FindVariableReferences {
409519
ValueConstructorVariant::LocalVariable {
410520
location: definition_location,
411521
..
412-
} if definition_location == self.definition_location => {
522+
} if definition_location == self.definition_origin_location() => {
413523
_ = self.references.insert(VariableReference {
414524
location: *location,
415525
kind: VariableReferenceKind::Variable,
@@ -426,7 +536,7 @@ impl<'ast> Visit<'ast> for FindVariableReferences {
426536
_type_: &'ast std::sync::Arc<Type>,
427537
definition_location: &'ast SrcSpan,
428538
) {
429-
if *definition_location == self.definition_location {
539+
if *definition_location == self.definition_origin_location() {
430540
_ = self.references.insert(VariableReference {
431541
location: *location,
432542
kind: VariableReferenceKind::Variable,
@@ -440,7 +550,7 @@ impl<'ast> Visit<'ast> for FindVariableReferences {
440550
// of the target variable.
441551
if clause
442552
.pattern_location()
443-
.contains(self.definition_location.start)
553+
.contains(self.definition_origin_location().start)
444554
{
445555
self.alternative_variable = AlternativeVariable::Track;
446556
}
@@ -476,15 +586,45 @@ impl<'ast> Visit<'ast> for FindVariableReferences {
476586
// the exact variable though, as that would result in a duplicated
477587
// reference.
478588
AlternativeVariable::Track
479-
if *name == self.name && *location != self.definition_location =>
589+
if *name == self.name && *location != self.definition_location() =>
480590
{
591+
self.update_alternative_origin(*location);
592+
593+
_ = self.references.insert(VariableReference {
594+
location: *location,
595+
kind: VariableReferenceKind::Variable,
596+
});
597+
}
598+
AlternativeVariable::Track | AlternativeVariable::Ignore => {}
599+
}
600+
}
601+
602+
fn visit_typed_pattern_assign(
603+
&mut self,
604+
location: &'ast SrcSpan,
605+
name: &'ast EcoString,
606+
pattern: &'ast ast::TypedPattern,
607+
) {
608+
match self.alternative_variable {
609+
// If we are inside the same alternative pattern as the target
610+
// variable and the name is the same, this is an alternative definition
611+
// of the same variable. We don't register the reference if this is
612+
// the exact variable though, as that would result in a duplicated
613+
// reference.
614+
AlternativeVariable::Track
615+
if *name == self.name && *location != self.definition_location() =>
616+
{
617+
self.update_alternative_origin(*location);
618+
481619
_ = self.references.insert(VariableReference {
482620
location: *location,
483621
kind: VariableReferenceKind::Variable,
484622
});
485623
}
486624
AlternativeVariable::Track | AlternativeVariable::Ignore => {}
487625
}
626+
627+
ast::visit::visit_typed_pattern_assign(self, location, name, pattern);
488628
}
489629

490630
fn visit_typed_bit_array_size_variable(
@@ -502,7 +642,7 @@ impl<'ast> Visit<'ast> for FindVariableReferences {
502642
ValueConstructorVariant::LocalVariable {
503643
location: definition_location,
504644
..
505-
} if *definition_location == self.definition_location => {
645+
} if *definition_location == self.definition_origin_location() => {
506646
_ = self.references.insert(VariableReference {
507647
location: *location,
508648
kind: VariableReferenceKind::Variable,
@@ -524,7 +664,7 @@ impl<'ast> Visit<'ast> for FindVariableReferences {
524664
location: definition_location,
525665
..
526666
} if arg.uses_label_shorthand()
527-
&& *definition_location == self.definition_location =>
667+
&& *definition_location == self.definition_origin_location() =>
528668
{
529669
_ = self.references.insert(VariableReference {
530670
location: *location,

compiler-core/src/language_server/tests/rename.rs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1387,6 +1387,119 @@ pub fn main(x) {
13871387
);
13881388
}
13891389

1390+
// https://github.com/gleam-lang/gleam/issues/5091
1391+
#[test]
1392+
fn rename_alternative_pattern_aliases() {
1393+
assert_rename!(
1394+
"
1395+
pub fn main(x) {
1396+
case x {
1397+
[] as list | [_] as list -> list
1398+
_ -> []
1399+
}
1400+
}
1401+
",
1402+
"new_name",
1403+
find_position_of("list")
1404+
);
1405+
}
1406+
1407+
#[test]
1408+
fn rename_alternative_pattern_aliases_from_alternative() {
1409+
assert_rename!(
1410+
"
1411+
pub fn main(x) {
1412+
case x {
1413+
[] as list | [_] as list -> list
1414+
_ -> []
1415+
}
1416+
}
1417+
",
1418+
"new_name",
1419+
find_position_of("list").nth_occurrence(2)
1420+
);
1421+
}
1422+
1423+
#[test]
1424+
fn rename_alternative_pattern_aliases_from_usage() {
1425+
assert_rename!(
1426+
"
1427+
pub fn main(x) {
1428+
case x {
1429+
[] as list | [_] as list -> list
1430+
_ -> []
1431+
}
1432+
}
1433+
",
1434+
"new_name",
1435+
find_position_of("list").nth_occurrence(3)
1436+
);
1437+
}
1438+
1439+
#[test]
1440+
fn rename_alternative_pattern_alias_and_variable_1() {
1441+
assert_rename!(
1442+
"
1443+
pub fn main(x) {
1444+
case x {
1445+
[] as list | [_, ..list] -> list
1446+
_ -> []
1447+
}
1448+
}
1449+
",
1450+
"new_name",
1451+
find_position_of("list").nth_occurrence(1)
1452+
);
1453+
}
1454+
1455+
#[test]
1456+
fn rename_alternative_pattern_alias_and_variable_2() {
1457+
assert_rename!(
1458+
"
1459+
pub fn main(x) {
1460+
case x {
1461+
[] as list | [_, ..list] -> list
1462+
_ -> []
1463+
}
1464+
}
1465+
",
1466+
"new_name",
1467+
find_position_of("list").nth_occurrence(2)
1468+
);
1469+
}
1470+
1471+
#[test]
1472+
fn rename_alternative_pattern_alias_and_variable_3() {
1473+
assert_rename!(
1474+
"
1475+
pub fn main(x) {
1476+
case x {
1477+
[_, ..list] | [] as list -> list
1478+
_ -> []
1479+
}
1480+
}
1481+
",
1482+
"new_name",
1483+
find_position_of("list").nth_occurrence(1)
1484+
);
1485+
}
1486+
1487+
#[test]
1488+
fn rename_alternative_pattern_alias_and_variable_4() {
1489+
assert_rename!(
1490+
"
1491+
pub fn main(x) {
1492+
case x {
1493+
[_, ..list] | [] as list -> list
1494+
_ -> []
1495+
}
1496+
}
1497+
",
1498+
"new_name",
1499+
find_position_of("list").nth_occurrence(2)
1500+
);
1501+
}
1502+
13901503
#[test]
13911504
fn rename_alternative_pattern_from_usage() {
13921505
assert_rename!(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
source: compiler-core/src/language_server/tests/rename.rs
3+
expression: "\npub fn main(x) {\n case x {\n [] as list | [_, ..list] -> list\n _ -> []\n }\n}\n"
4+
---
5+
----- BEFORE RENAME
6+
-- app.gleam
7+
8+
pub fn main(x) {
9+
case x {
10+
[] as list | [_, ..list] -> list
11+
↑▔▔▔
12+
_ -> []
13+
}
14+
}
15+
16+
17+
----- AFTER RENAME
18+
-- app.gleam
19+
20+
pub fn main(x) {
21+
case x {
22+
[] as new_name | [_, ..new_name] -> new_name
23+
_ -> []
24+
}
25+
}

0 commit comments

Comments
 (0)