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

feat(fmt): add functionality from prisma-fmt as quick fixes #4945

Merged
merged 11 commits into from
Jul 15, 2024
21 changes: 17 additions & 4 deletions prisma-fmt/src/code_actions.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod block;
mod field;
mod mongodb;
mod multi_schema;
mod relation_mode;
Expand Down Expand Up @@ -86,6 +87,10 @@ pub(crate) fn available_actions(
.walk_models_in_file(initiating_file_id)
.chain(validated_schema.db.walk_views_in_file(initiating_file_id))
{
for field in context.db.walk_fields(model.id) {
field::add_missing_opposite_relation(&mut actions, &context, field);
}

block::create_missing_block_for_model(&mut actions, &context, model);

if config.preview_features().contains(PreviewFeature::MultiSchema) {
Expand Down Expand Up @@ -114,20 +119,28 @@ pub(crate) fn available_actions(
}

for relation in validated_schema.db.walk_relations() {
if let RefinedRelationWalker::Inline(relation) = relation.refine() {
let complete_relation = match relation.as_complete() {
if let RefinedRelationWalker::Inline(inline_relation) = relation.refine() {
relations::add_referencing_side_relation(&mut actions, &context, inline_relation);

let complete_relation = match inline_relation.as_complete() {
Some(relation) => relation,
None => continue,
};

if matches!(datasource, Some(ds) if ds.active_provider != "mongodb") {
relations::make_referencing_side_many(&mut actions, &context, complete_relation);
}

relations::add_referenced_side_unique(&mut actions, &context, complete_relation);

if relation.is_one_to_one() {
if inline_relation.is_one_to_one() {
relations::add_referencing_side_unique(&mut actions, &context, complete_relation);
}

if validated_schema.relation_mode().is_prisma()
&& relation.referencing_model().is_defined_in_file(initiating_file_id)
&& inline_relation
.referencing_model()
.is_defined_in_file(initiating_file_id)
{
relations::add_index_for_relation_fields(&mut actions, &context, complete_relation.referencing_field());
}
Expand Down
82 changes: 82 additions & 0 deletions prisma-fmt/src/code_actions/field.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use lsp_types::{CodeAction, CodeActionKind, CodeActionOrCommand};
use psl::{
diagnostics::Span,
parser_database::walkers::{self, FieldWalker},
schema_ast::ast::{WithAttributes, WithName, WithSpan},
};

use super::CodeActionsContext;

pub(super) fn add_missing_opposite_relation(
actions: &mut Vec<CodeActionOrCommand>,
context: &CodeActionsContext<'_>,
field: FieldWalker<'_>,
) {
match field.refine() {
Some(walkers::RefinedFieldWalker::Relation(_)) => (),
_ => return,
}

let name = field.model().name();
let target_name = field.ast_field().field_type.name();
let diagnostics = context.diagnostics_for_span_with_message(
field.ast_field().span(),
"is missing an opposite relation field on the model",
);

if diagnostics.is_empty() {
return;
}

let Some(target_model) = context.db.find_model(target_name) else {
return;
};

let target_file_id = target_model.file_id();
let target_file_content = context.db.source(target_file_id);

let span = Span {
start: target_model.ast_model().span().end - 1,
end: target_model.ast_model().span().end - 1,
file_id: target_file_id,
};

let separator = if target_model.ast_model().attributes().is_empty() {
target_model.newline().to_string()
} else {
Default::default()
};
let indentation = target_model.indentation();
let newline = target_model.newline();

let name_arg = field
.ast_field()
.attributes
.iter()
.find(|attr| attr.name() == "relation")
.and_then(|attr| attr.arguments.arguments.iter().find(|arg| arg.value.is_string()));

let relation = name_arg.map_or(Default::default(), |arg| format!(" @relation({})", arg));

let formatted_content = format!("{separator}{indentation}{name} {name}[]{relation}{newline}");

let Ok(edit) = super::create_text_edit(
context.db.file_name(target_file_id),
target_file_content,
formatted_content,
false,
span,
) else {
return;
};

let action = CodeAction {
title: format!("Add missing relation field to model {}", target_name),
kind: Some(CodeActionKind::QUICKFIX),
edit: Some(edit),
diagnostics: Some(diagnostics),
..Default::default()
};

actions.push(CodeActionOrCommand::CodeAction(action))
}
211 changes: 208 additions & 3 deletions prisma-fmt/src/code_actions/relations.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use lsp_types::{CodeAction, CodeActionKind, CodeActionOrCommand, TextEdit, WorkspaceEdit};
use psl::parser_database::{
ast::WithSpan,
walkers::{CompleteInlineRelationWalker, RelationFieldWalker},
walkers::{CompleteInlineRelationWalker, InlineRelationWalker, RelationFieldWalker},
};
use std::collections::HashMap;

Expand Down Expand Up @@ -197,6 +197,209 @@ pub(super) fn add_referenced_side_unique(
actions.push(CodeActionOrCommand::CodeAction(action));
}

/// If the referencing side of the relation does not include
/// a complete relation attribute.
///
/// If it includes no relation attribute:
///
/// ```prisma
/// model interm {
/// id Int @id
/// forumId Int
/// forum Forum
/// // ^^^ suggests `@relation(fields: [], references: [])`
/// }
/// ```
///
/// If it includes an empty relation attribute:
///
/// ```prisma
/// model interm {
/// id Int @id
/// forumId Int
/// forum Forum @relation( )
/// // ^^^ suggests `fields: [], references: []``
/// }
/// ```
///
/// ```prisma
///
/// model Forum {
/// id Int @id
/// name String
///
/// interm interm[]
/// }
/// ```
pub(super) fn add_referencing_side_relation(
actions: &mut Vec<CodeActionOrCommand>,
ctx: &CodeActionsContext<'_>,
relation: InlineRelationWalker<'_>,
) {
let Some(initiating_field) = relation.forward_relation_field() else {
return;
};

// * Full example diagnostic message:
// ! Error parsing attribute "@relation":
// ! The relation field `forum` on Model `Interm` must specify
// ! the `fields` argument in the @relation attribute.
// ! You can run `prisma format` to fix this automatically.
let mut diagnostics = ctx.diagnostics_for_span_with_message(
initiating_field.ast_field().span(),
"must specify the `fields` argument in the @relation attribute.",
);

// ? (@druue) We seem to have a slightly different message for effectively the same schema state
// * Full example diagnostic message:
// ! Error parsing attribute "@relation":
// ! The relation fields `wife` on Model `User` and `husband` on Model `User`
// ! do not provide the `fields` argument in the @relation attribute.
// ! You have to provide it on one of the two fields.
diagnostics.extend(ctx.diagnostics_for_span_with_message(
initiating_field.ast_field().span(),
"do not provide the `fields` argument in the @relation attribute.",
));

if diagnostics.is_empty() {
return;
}

let pk = relation.referenced_model().primary_key();
let newline = relation.referenced_model().newline();

let Some((reference_ids, field_ids, fields)) = pk.map(|pk| {
let fields = pk.fields();
let (names, (field_ids, fields)): (Vec<&str>, (Vec<String>, Vec<String>)) = fields
.map(|f| {
let field_name = f.name();
let field_id = format!("{}{}", relation.referenced_model().name(), field_name);
let field_full = format!("{} {}?", field_id, f.ast_field().field_type.name());

(field_name, (field_id, field_full))
})
.unzip();

(
names.join(", "),
field_ids.join(", "),
format!("\n{}{}", fields.join(newline.as_ref()), newline),
)
}) else {
return;
};

let references = format!("references: [{reference_ids}]");
let fields_arg = format!("fields: [{field_ids}]");

// * In the prisma-fmt incarnation of this, we assume:
// * - fields contains a field with the name `referenced_modelId`
// * - references contains a field named `id`
let (range, new_text) = match initiating_field.relation_attribute() {
Some(attr) => {
let name = attr
.arguments
.arguments
.iter()
.find(|arg| arg.value.is_string())
.map_or(Default::default(), |arg| format!("{arg}, "));

let new_text = format!("@relation({}{}, {})", name, fields_arg, references);
let range = super::span_to_range(attr.span(), ctx.initiating_file_source());

(range, new_text)
}
None => {
let new_text = format!(
" @relation({}, {}){}",
fields_arg,
references,
initiating_field.model().newline()
);
let range = super::range_after_span(initiating_field.ast_field().span(), ctx.initiating_file_source());

(range, new_text)
}
};

let mut changes: HashMap<lsp_types::Url, Vec<TextEdit>> = HashMap::new();
changes.insert(
ctx.params.text_document.uri.clone(),
vec![
TextEdit { range, new_text },
TextEdit {
range: super::range_after_span(initiating_field.ast_field().span(), ctx.initiating_file_source()),
new_text: fields,
},
],
);

let edit = WorkspaceEdit {
changes: Some(changes),
..Default::default()
};

let action = CodeAction {
title: String::from("Add relation attribute for relation field"),
kind: Some(CodeActionKind::QUICKFIX),
edit: Some(edit),
diagnostics: Some(diagnostics),
..Default::default()
};

actions.push(CodeActionOrCommand::CodeAction(action))
}

pub(super) fn make_referencing_side_many(
actions: &mut Vec<CodeActionOrCommand>,
ctx: &CodeActionsContext<'_>,
relation: CompleteInlineRelationWalker<'_>,
) {
let initiating_field = relation.referencing_field();

// * Full example diagnostic message:
// ! Error parsing attribute "@relation":
// ! The relation field `forum` on Model `Interm` must specify
// ! the `fields` argument in the @relation attribute.
// ! You can run `prisma format` to fix this automatically.
let diagnostics = ctx.diagnostics_for_span_with_message(
initiating_field.ast_field().span(),
"must specify the `fields` argument in the @relation attribute.",
);

if diagnostics.is_empty() {
return;
}

let text = match initiating_field.relation_attribute() {
Some(_) => return,
None => {
let new_text = format!("[]{}", initiating_field.model().newline());
let range = super::range_after_span(initiating_field.ast_field().span(), ctx.initiating_file_source());

TextEdit { range, new_text }
}
};

let mut changes: HashMap<lsp_types::Url, Vec<TextEdit>> = HashMap::new();
changes.insert(ctx.params.text_document.uri.clone(), vec![text]);

let edit = WorkspaceEdit {
changes: Some(changes),
..Default::default()
};

let action = CodeAction {
title: String::from("Mark relation field as many `[]`"),
kind: Some(CodeActionKind::QUICKFIX),
edit: Some(edit),
diagnostics: Some(diagnostics),
..Default::default()
};

actions.push(CodeActionOrCommand::CodeAction(action))
}

/// For schema's with emulated relations,
/// If the referenced side of the relation does not point to a unique
/// constraint, the action adds the attribute.
Expand Down Expand Up @@ -285,8 +488,10 @@ pub(super) fn add_index_for_relation_fields(
..Default::default()
};

let diagnostics = context
.diagnostics_for_span_with_message(relation.relation_attribute().unwrap().span, "relationMode = \"prisma\"");
let diagnostics = context.diagnostics_for_span_with_message(
relation.relation_attribute().unwrap().span(),
"relationMode = \"prisma\"",
);

if diagnostics.is_empty() {
return;
Expand Down
Loading
Loading