Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added extract constant code action #4228

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

matiascr
Copy link

@matiascr matiascr commented Feb 10, 2025

Addresses #3936

@matiascr matiascr force-pushed the extract_const_code_action branch from 3ebaaa4 to 3ccbe21 Compare February 10, 2025 21:23
@matiascr matiascr force-pushed the extract_const_code_action branch 2 times, most recently from bd8e305 to 7f8aef4 Compare February 11, 2025 17:40
@matiascr matiascr marked this pull request as ready for review February 11, 2025 17:41
@GearsDatapacks
Copy link
Member

FYI before this can be merged, you'll need to add tests. Those can be found in the language_server/tests/action.rs file. (It also helps us review changes as the snapshots show the output format of the action!)
Also, you will need to update CHANGELOG.md to include this change.

@matiascr matiascr force-pushed the extract_const_code_action branch from d193b39 to 11ba75b Compare February 11, 2025 22:39
@matiascr matiascr marked this pull request as draft February 12, 2025 20:25
@matiascr matiascr marked this pull request as ready for review February 12, 2025 22:52
@matiascr matiascr force-pushed the extract_const_code_action branch from fbdf675 to 5398646 Compare February 14, 2025 13:37
@lpil
Copy link
Member

lpil commented Feb 14, 2025

Thank you both!

Copy link
Member

@lpil lpil left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!! Looking very nice.

Could you add tests for these please:

  • Expressions with function calls
  • Expressions using local variables
  • Expressions using records
  • Expressions use constants
  • Expression with bool operators
  • Expressions with logical operators
  • When the name is taken by a const
  • When the name is taken by a function

It would also be good to take local variables into account for the naming.

})
.collect(),
};
name_generator.reserve_variable_names(module_constant_names);
Copy link
Member

@lpil lpil Feb 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functions would also want to be taken into account here. Perhaps this would fit best in the walker? I've not looked at how we do it in the others.

Comment on lines 2938 to 3025
if let Some(function_start) = container_function_start {
self.edits.insert(
function_start,
format!("const {variable_name} = {content}\n\n"),
);

self.edits
.replace(expression_span, String::from(variable_name));
}

let mut action = Vec::with_capacity(1);
CodeActionBuilder::new("Extract constant")
.kind(CodeActionKind::REFACTOR_EXTRACT)
.changes(self.params.text_document.uri.clone(), self.edits.edits)
.preferred(false)
.push_to(&mut action);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When is it possible for this to ever be None? It looks like in that case it would add a refactoring that doesn't contain any edits?

}
}

ast::visit::visit_typed_expr(self, expr);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we want to stop walking the tree if the expression has already been found?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite, it's required to continue in case we want to extract a literal that's inside, say, a function call or record constructor. Maybe it won't be needed after applying the rest of the changes, right now it's necessary.

Comment on lines 2928 to 2936
let container_function_start = self
.module
.ast
.definitions
.iter()
.filter(|definition| definition.is_function())
.map(|function| function.location().start)
.filter(|function_start| function_start < &expression_span.start)
.max();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we do this during the tree visiting?

@lpil lpil marked this pull request as draft February 17, 2025 17:40
@matiascr matiascr force-pushed the extract_const_code_action branch 2 times, most recently from a00559b to 4e8e361 Compare February 20, 2025 09:11
@lpil
Copy link
Member

lpil commented Feb 23, 2025

If you are ready for a review please un-draft the PR 🙏

@matiascr matiascr force-pushed the extract_const_code_action branch from 83753c0 to 10d41e2 Compare February 23, 2025 12:45
@matiascr matiascr marked this pull request as ready for review February 23, 2025 12:45
Copy link
Member

@lpil lpil left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fab work! Thank you!

I've left some comments inline.

Could you add these tests also:

  • Extracting results
  • Extracting Nil
  • Extracting bit arrays
  • Extracting a custom type that has fields (a record)
  • Extracting expressions which reference constant variables
  • Extracting expressions from assignments that use complex patterns (the assignment can stay and just the value is replaced)
  • Refusing to extract expressions which reference non-constant variables
  • Refusing to extract expressions which call functions

And anything else you can think of. I may have missed something 😁

Thank you!

constructor.variant,
type_::ValueConstructorVariant::Record { arity: 0, .. }
),
_ => false,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No catch-all pattern please! They cause bugs as you don't have to update the code when new ones are added.

The nicest way to do this will be to move all the guards into the body of the clause, to avoid repeating each variant twice (once with a guard, once without).

Assignment,
}

fn is_literal_or_made_of_literals(expr: &TypedExpr) -> bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A list of literals is a literal also.

Suggested change
fn is_literal_or_made_of_literals(expr: &TypedExpr) -> bool {
fn is_literal(expr: &TypedExpr) -> bool {

Though I think it is really trying to work out is if the expression is valid constant expression syntax, which you can't tell by looking at just the AST. You'd need to know if variables point to constants or not, and if all the calls are to record constructors.

true
}
// Attempt to extract whole bit array as long as it's made up of literals
TypedExpr::BitArray { segments, .. }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You would need to check the options also to check for any variable usage there

value_to_use: Option<EcoString>,
}

enum ExtractableToConstant {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you document what this is and how it is used please, including each variant.

self.edits.delete(expr_span_with_new_line);
}

ExtractableToConstant::Collection | ExtractableToConstant::Value => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these two exactly the same then?

Comment on lines +3098 to +3101
if matches!(
self.type_of_extractable,
None | Some(ExtractableToConstant::Collection)
) && is_literal_or_made_of_literals(expr)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this bit doing? Would be useful to have documentation comments explaining the intent

Comment on lines +3111 to +3114
TypedExpr::Int { .. } | TypedExpr::Float { .. } | TypedExpr::String { .. } => {
ExtractableToConstant::Value
}
_ => ExtractableToConstant::Collection,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mapping of types to ExtractableToConstant isn't quite right. They're all values, and there are things that are not containers being marked as containers here.

container_function_start: Option<u32>,
type_of_extractable: Option<ExtractableToConstant>,
name_to_use: Option<EcoString>,
value_to_use: Option<EcoString>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be useful to have these documented as it's a little unclear what they are.

What's the different between "value to use" and "selected expression"?

"type of extractable" seems to not be a type.

@lpil lpil marked this pull request as draft February 23, 2025 22:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants