From 5954db1fa6f2b12aeb1d5cf503b870c3bb6f8387 Mon Sep 17 00:00:00 2001 From: Sophie <29753584+Druue@users.noreply.github.com> Date: Wed, 10 Jul 2024 14:38:18 +0200 Subject: [PATCH] feat(fmt): lsp hover (#4923) * adds support for LSP hover * updated the parser database so that it doesn't immediately bail out on schema validation errors. The QE will still only accept fully valid schemas so the parser database the QE interacts with will still only contain fully valid data, but prisma-fmt needs to be able to interact with incomplete schemas. * Updated enum values to actually use `EnumValueId`s rather than bare `u32`s --------- Co-authored-by: Sergey Tatarintsev Co-authored-by: Flavian Desverne --- prisma-fmt/src/hover.rs | 244 ++++++++++++++++++ prisma-fmt/src/lib.rs | 28 +- prisma-fmt/src/references.rs | 5 +- prisma-fmt/tests/code_actions/test_api.rs | 3 +- prisma-fmt/tests/hover/mod.rs | 2 + .../composite_from_block_name/result.json | 6 + .../composite_from_block_name/schema.prisma | 19 ++ .../composite_from_field_type/result.json | 6 + .../composite_from_field_type/schema.prisma | 14 + .../embedded_m2n_mongodb/result.json | 6 + .../embedded_m2n_mongodb/schema.prisma | 16 ++ .../enum_from_block_name/result.json | 6 + .../enum_from_block_name/schema.prisma | 20 ++ .../enum_from_field_type/result.json | 6 + .../enum_from_field_type/schema.prisma | 16 ++ .../result.json | 6 + .../schema.prisma | 15 ++ .../field_from_model_field_name/result.json | 6 + .../field_from_model_field_name/schema.prisma | 15 ++ .../model_from_block_name/result.json | 6 + .../model_from_block_name/schema.prisma | 20 ++ .../result.json | 6 + .../schema.prisma | 26 ++ .../result.json | 1 + .../schema.prisma | 12 + .../scenarios/model_from_view_type/a.prisma | 6 + .../scenarios/model_from_view_type/b.prisma | 5 + .../model_from_view_type/config.prisma | 9 + .../model_from_view_type/result.json | 6 + .../one_to_many_self_relation/result.json | 6 + .../one_to_many_self_relation/schema.prisma | 6 + .../value_from_enum_value_name/result.json | 6 + .../value_from_enum_value_name/schema.prisma | 15 ++ prisma-fmt/tests/hover/test_api.rs | 144 +++++++++++ prisma-fmt/tests/hover/tests.rs | 29 +++ prisma-fmt/tests/hover_tests.rs | 2 + prisma-schema-wasm/src/lib.rs | 9 + psl/parser-database/src/attributes.rs | 51 ++-- psl/parser-database/src/context.rs | 4 +- psl/parser-database/src/lib.rs | 26 -- psl/parser-database/src/relations.rs | 8 +- psl/parser-database/src/types.rs | 18 +- .../src/walkers/composite_type.rs | 17 ++ psl/parser-database/src/walkers/enum.rs | 22 +- psl/parser-database/src/walkers/field.rs | 16 +- psl/parser-database/src/walkers/model.rs | 5 + psl/parser-database/src/walkers/relation.rs | 28 +- .../src/walkers/relation_field.rs | 10 +- .../src/walkers/scalar_field.rs | 13 + psl/psl/tests/attributes/composite_index.rs | 6 + psl/psl/tests/base/basic.rs | 6 + ...index_attributes_on_composite_types.prisma | 12 + ...elation_field_attribute_not_allowed.prisma | 6 + psl/schema-ast/src/ast/attribute.rs | 10 +- psl/schema-ast/src/ast/enum.rs | 6 + psl/schema-ast/src/ast/field.rs | 16 ++ psl/schema-ast/src/ast/find_at_position.rs | 2 +- .../src/ast/find_at_position/enum.rs | 5 + .../src/ast_builders/datamodel_ast_builder.rs | 4 +- 59 files changed, 938 insertions(+), 106 deletions(-) create mode 100644 prisma-fmt/src/hover.rs create mode 100644 prisma-fmt/tests/hover/mod.rs create mode 100644 prisma-fmt/tests/hover/scenarios/composite_from_block_name/result.json create mode 100644 prisma-fmt/tests/hover/scenarios/composite_from_block_name/schema.prisma create mode 100644 prisma-fmt/tests/hover/scenarios/composite_from_field_type/result.json create mode 100644 prisma-fmt/tests/hover/scenarios/composite_from_field_type/schema.prisma create mode 100644 prisma-fmt/tests/hover/scenarios/embedded_m2n_mongodb/result.json create mode 100644 prisma-fmt/tests/hover/scenarios/embedded_m2n_mongodb/schema.prisma create mode 100644 prisma-fmt/tests/hover/scenarios/enum_from_block_name/result.json create mode 100644 prisma-fmt/tests/hover/scenarios/enum_from_block_name/schema.prisma create mode 100644 prisma-fmt/tests/hover/scenarios/enum_from_field_type/result.json create mode 100644 prisma-fmt/tests/hover/scenarios/enum_from_field_type/schema.prisma create mode 100644 prisma-fmt/tests/hover/scenarios/field_from_composite_field_name/result.json create mode 100644 prisma-fmt/tests/hover/scenarios/field_from_composite_field_name/schema.prisma create mode 100644 prisma-fmt/tests/hover/scenarios/field_from_model_field_name/result.json create mode 100644 prisma-fmt/tests/hover/scenarios/field_from_model_field_name/schema.prisma create mode 100644 prisma-fmt/tests/hover/scenarios/model_from_block_name/result.json create mode 100644 prisma-fmt/tests/hover/scenarios/model_from_block_name/schema.prisma create mode 100644 prisma-fmt/tests/hover/scenarios/model_from_model_type_includes_broken_relations/result.json create mode 100644 prisma-fmt/tests/hover/scenarios/model_from_model_type_includes_broken_relations/schema.prisma create mode 100644 prisma-fmt/tests/hover/scenarios/model_from_model_type_on_broken_relations/result.json create mode 100644 prisma-fmt/tests/hover/scenarios/model_from_model_type_on_broken_relations/schema.prisma create mode 100644 prisma-fmt/tests/hover/scenarios/model_from_view_type/a.prisma create mode 100644 prisma-fmt/tests/hover/scenarios/model_from_view_type/b.prisma create mode 100644 prisma-fmt/tests/hover/scenarios/model_from_view_type/config.prisma create mode 100644 prisma-fmt/tests/hover/scenarios/model_from_view_type/result.json create mode 100644 prisma-fmt/tests/hover/scenarios/one_to_many_self_relation/result.json create mode 100644 prisma-fmt/tests/hover/scenarios/one_to_many_self_relation/schema.prisma create mode 100644 prisma-fmt/tests/hover/scenarios/value_from_enum_value_name/result.json create mode 100644 prisma-fmt/tests/hover/scenarios/value_from_enum_value_name/schema.prisma create mode 100644 prisma-fmt/tests/hover/test_api.rs create mode 100644 prisma-fmt/tests/hover/tests.rs create mode 100644 prisma-fmt/tests/hover_tests.rs diff --git a/prisma-fmt/src/hover.rs b/prisma-fmt/src/hover.rs new file mode 100644 index 000000000000..ae2eaa28e1da --- /dev/null +++ b/prisma-fmt/src/hover.rs @@ -0,0 +1,244 @@ +use log::warn; +use lsp_types::{Hover, HoverContents, HoverParams, MarkupContent, MarkupKind}; +use psl::{ + error_tolerant_parse_configuration, + parser_database::{ + walkers::{self, Walker}, + ParserDatabase, RelationFieldId, ScalarFieldType, + }, + schema_ast::ast::{ + self, CompositeTypePosition, EnumPosition, EnumValuePosition, Field, FieldPosition, ModelPosition, + SchemaPosition, WithDocumentation, WithName, + }, + Diagnostics, SourceFile, +}; + +use crate::{offsets::position_to_offset, LSPContext}; + +pub(super) type HoverContext<'a> = LSPContext<'a, HoverParams>; + +impl<'a> HoverContext<'a> { + pub(super) fn position(&self) -> Option { + let pos = self.params.text_document_position_params.position; + let initiating_doc = self.initiating_file_source(); + + position_to_offset(&pos, initiating_doc) + } +} + +pub fn run(schema_files: Vec<(String, SourceFile)>, params: HoverParams) -> Option { + let (_, config, _) = error_tolerant_parse_configuration(&schema_files); + + let db = { + let mut diag = Diagnostics::new(); + ParserDatabase::new(&schema_files, &mut diag) + }; + + let Some(initiating_file_id) = db.file_id(params.text_document_position_params.text_document.uri.as_str()) else { + warn!("Initiating file name is not found in the schema"); + return None; + }; + + let ctx = HoverContext { + db: &db, + config: &config, + initiating_file_id, + params: ¶ms, + }; + + hover(ctx) +} + +fn hover(ctx: HoverContext<'_>) -> Option { + let position = match ctx.position() { + Some(pos) => pos, + None => { + warn!("Received a position outside of the document boundaries in HoverParams"); + return None; + } + }; + + let ast = ctx.db.ast(ctx.initiating_file_id); + let contents = match ast.find_at_position(position) { + SchemaPosition::TopLevel => None, + + // --- Block Names --- + SchemaPosition::Model(model_id, ModelPosition::Name(name)) => { + let model = ctx.db.walk((ctx.initiating_file_id, model_id)).ast_model(); + let variant = if model.is_view() { "view" } else { "model" }; + + Some(format_hover_content( + model.documentation().unwrap_or(""), + variant, + name, + None, + )) + } + SchemaPosition::Enum(enum_id, EnumPosition::Name(name)) => { + let enm = ctx.db.walk((ctx.initiating_file_id, enum_id)).ast_enum(); + Some(hover_enum(enm, name)) + } + SchemaPosition::CompositeType(ct_id, CompositeTypePosition::Name(name)) => { + let ct = ctx.db.walk((ctx.initiating_file_id, ct_id)).ast_composite_type(); + Some(hover_composite(ct, name)) + } + + // --- Block Field Names --- + SchemaPosition::Model(model_id, ModelPosition::Field(field_id, FieldPosition::Name(name))) => { + let field = ctx + .db + .walk((ctx.initiating_file_id, model_id)) + .field(field_id) + .ast_field(); + + Some(format_hover_content( + field.documentation().unwrap_or_default(), + "field", + name, + None, + )) + } + SchemaPosition::CompositeType(ct_id, CompositeTypePosition::Field(field_id, FieldPosition::Name(name))) => { + let field = ctx.db.walk((ctx.initiating_file_id, ct_id)).field(field_id).ast_field(); + + Some(format_hover_content( + field.documentation().unwrap_or_default(), + "field", + name, + None, + )) + } + SchemaPosition::Enum(enm_id, EnumPosition::Value(value_id, EnumValuePosition::Name(name))) => { + let value = ctx + .db + .walk((ctx.initiating_file_id, enm_id)) + .value(value_id) + .ast_value(); + + Some(format_hover_content( + value.documentation().unwrap_or_default(), + "value", + name, + None, + )) + } + + // --- Block Field Types --- + SchemaPosition::Model(model_id, ModelPosition::Field(field_id, FieldPosition::Type(name))) => { + let initiating_field = &ctx.db.walk((ctx.initiating_file_id, model_id)).field(field_id); + + initiating_field.refine().and_then(|field| match field { + walkers::RefinedFieldWalker::Scalar(scalar) => match scalar.scalar_field_type() { + ScalarFieldType::CompositeType(_) => { + let ct = scalar.field_type_as_composite_type().unwrap().ast_composite_type(); + Some(hover_composite(ct, ct.name())) + } + ScalarFieldType::Enum(_) => { + let enm = scalar.field_type_as_enum().unwrap().ast_enum(); + Some(hover_enum(enm, enm.name())) + } + _ => None, + }, + walkers::RefinedFieldWalker::Relation(rf) => { + let opposite_model = rf.related_model(); + let relation_info = rf.opposite_relation_field().map(|rf| (rf, rf.ast_field())); + let related_model_type = if opposite_model.ast_model().is_view() { + "view" + } else { + "model" + }; + + Some(format_hover_content( + opposite_model.ast_model().documentation().unwrap_or_default(), + related_model_type, + name, + relation_info, + )) + } + }) + } + + SchemaPosition::CompositeType(ct_id, CompositeTypePosition::Field(field_id, FieldPosition::Type(_))) => { + let field = &ctx.db.walk((ctx.initiating_file_id, ct_id)).field(field_id); + match field.r#type() { + psl::parser_database::ScalarFieldType::CompositeType(_) => { + let ct = field.field_type_as_composite_type().unwrap().ast_composite_type(); + Some(hover_composite(ct, ct.name())) + } + psl::parser_database::ScalarFieldType::Enum(_) => { + let enm = field.field_type_as_enum().unwrap().ast_enum(); + Some(hover_enum(enm, enm.name())) + } + _ => None, + } + } + _ => None, + }; + + contents.map(|contents| Hover { contents, range: None }) +} + +fn hover_enum(enm: &ast::Enum, name: &str) -> HoverContents { + format_hover_content(enm.documentation().unwrap_or_default(), "enum", name, None) +} + +fn hover_composite(ct: &ast::CompositeType, name: &str) -> HoverContents { + format_hover_content(ct.documentation().unwrap_or_default(), "type", name, None) +} + +fn format_hover_content( + documentation: &str, + variant: &str, + name: &str, + relation: Option<(Walker, &Field)>, +) -> HoverContents { + let fancy_line_break = String::from("\n___\n"); + + let (field, relation_kind) = format_relation_info(relation, &fancy_line_break); + + let prisma_display = match variant { + "model" | "enum" | "view" | "type" => { + format!("```prisma\n{variant} {name} {{{field}}}\n```{fancy_line_break}{relation_kind}") + } + "field" | "value" => format!("```prisma\n{name}\n```{fancy_line_break}"), + _ => "".to_owned(), + }; + let full_signature = format!("{prisma_display}{documentation}"); + + HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value: full_signature, + }) +} + +fn format_relation_info( + relation: Option<(Walker, &Field)>, + fancy_line_break: &String, +) -> (String, String) { + if let Some((rf, field)) = relation { + let relation = rf.relation(); + + let fields = rf + .referencing_fields() + .map(|fields| fields.map(|f| f.to_string()).collect::>().join(", ")) + .map_or_else(String::new, |fields| format!(", fields: [{fields}]")); + + let references = rf + .referenced_fields() + .map(|fields| fields.map(|f| f.to_string()).collect::>().join(", ")) + .map_or_else(String::new, |fields| format!(", references: [{fields}]")); + + let self_relation = if relation.is_self_relation() { " on self" } else { "" }; + let relation_kind = format!("{}{}", relation.relation_kind(), self_relation); + + let relation_name = relation.relation_name(); + let relation_inner = format!("name: \"{relation_name}\"{fields}{references}"); + + ( + format!("\n\t...\n\t{field} @relation({relation_inner})\n"), + format!("{relation_kind}{fancy_line_break}"), + ) + } else { + ("".to_owned(), "".to_owned()) + } +} diff --git a/prisma-fmt/src/lib.rs b/prisma-fmt/src/lib.rs index e21c6eef97e9..b6b13c47838f 100644 --- a/prisma-fmt/src/lib.rs +++ b/prisma-fmt/src/lib.rs @@ -3,23 +3,25 @@ mod code_actions; mod get_config; mod get_datamodel; mod get_dmmf; +mod hover; mod lint; mod merge_schemas; mod native; -mod offsets; mod preview; mod references; mod schema_file_input; mod text_document_completion; mod validate; +pub mod offsets; + use log::*; -pub use offsets::span_to_range; use psl::{ datamodel_connector::Connector, diagnostics::FileId, parser_database::ParserDatabase, Configuration, Datasource, Generator, }; use schema_file_input::SchemaFileInput; +use serde_json::json; #[derive(Debug, Clone, Copy)] pub(crate) struct LSPContext<'a, T> { @@ -110,6 +112,28 @@ pub fn references(schema_files: String, params: &str) -> String { serde_json::to_string(&references).unwrap() } +pub fn hover(schema_files: String, params: &str) -> String { + let schema: SchemaFileInput = match serde_json::from_str(&schema_files) { + Ok(schema) => schema, + Err(serde_err) => { + warn!("Failed to deserialize SchemaFileInput: {serde_err}"); + return json!(null).to_string(); + } + }; + + let params: lsp_types::HoverParams = match serde_json::from_str(params) { + Ok(params) => params, + Err(_) => { + warn!("Failed to deserialize Hover"); + return json!(null).to_string(); + } + }; + + let hover = hover::run(schema.into(), params); + + serde_json::to_string(&hover).unwrap() +} + /// The two parameters are: /// - The [`SchemaFileInput`] to reformat, as a string. /// - An LSP diff --git a/prisma-fmt/src/references.rs b/prisma-fmt/src/references.rs index 00da04548893..0fb3b3a665ad 100644 --- a/prisma-fmt/src/references.rs +++ b/prisma-fmt/src/references.rs @@ -11,7 +11,10 @@ use psl::{ Diagnostics, SourceFile, }; -use crate::{offsets::position_to_offset, span_to_range, LSPContext}; +use crate::{ + offsets::{position_to_offset, span_to_range}, + LSPContext, +}; pub(super) type ReferencesContext<'a> = LSPContext<'a, ReferenceParams>; diff --git a/prisma-fmt/tests/code_actions/test_api.rs b/prisma-fmt/tests/code_actions/test_api.rs index 396dfd103a2c..b09f517be9c5 100644 --- a/prisma-fmt/tests/code_actions/test_api.rs +++ b/prisma-fmt/tests/code_actions/test_api.rs @@ -1,6 +1,7 @@ use lsp_types::{Diagnostic, DiagnosticSeverity}; use once_cell::sync::Lazy; -use prisma_fmt::span_to_range; + +use prisma_fmt::offsets::span_to_range; use psl::{diagnostics::Span, SourceFile}; use std::{fmt::Write as _, io::Write as _, path::PathBuf}; diff --git a/prisma-fmt/tests/hover/mod.rs b/prisma-fmt/tests/hover/mod.rs new file mode 100644 index 000000000000..cf3a59fec326 --- /dev/null +++ b/prisma-fmt/tests/hover/mod.rs @@ -0,0 +1,2 @@ +mod test_api; +mod tests; diff --git a/prisma-fmt/tests/hover/scenarios/composite_from_block_name/result.json b/prisma-fmt/tests/hover/scenarios/composite_from_block_name/result.json new file mode 100644 index 000000000000..db452d702e44 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/composite_from_block_name/result.json @@ -0,0 +1,6 @@ +{ + "contents": { + "kind": "markdown", + "value": "```prisma\ntype TypeA {}\n```\n___\nThis is doc for TypeA" + } +} \ No newline at end of file diff --git a/prisma-fmt/tests/hover/scenarios/composite_from_block_name/schema.prisma b/prisma-fmt/tests/hover/scenarios/composite_from_block_name/schema.prisma new file mode 100644 index 000000000000..2f0102f18cf2 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/composite_from_block_name/schema.prisma @@ -0,0 +1,19 @@ +generator js { + provider = "prisma-client-js" +} + +datasource db { + provider = "mongodb" + url = env("DATABASE_URL") +} + +model ModelNameA { + id String @id @map("_id") + bId Int + val TypeA +} + +/// This is doc for TypeA +type Typ<|>eA { +id String +} diff --git a/prisma-fmt/tests/hover/scenarios/composite_from_field_type/result.json b/prisma-fmt/tests/hover/scenarios/composite_from_field_type/result.json new file mode 100644 index 000000000000..fe56452d5653 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/composite_from_field_type/result.json @@ -0,0 +1,6 @@ +{ + "contents": { + "kind": "markdown", + "value": "```prisma\ntype Address {}\n```\n___\nAddress Doc" + } +} \ No newline at end of file diff --git a/prisma-fmt/tests/hover/scenarios/composite_from_field_type/schema.prisma b/prisma-fmt/tests/hover/scenarios/composite_from_field_type/schema.prisma new file mode 100644 index 000000000000..e43eca95795e --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/composite_from_field_type/schema.prisma @@ -0,0 +1,14 @@ +datasource db { + provider = "mongodb" + url = env("DATABASE_URL") +} + +model User { + id String @id @map("_id") + address Add<|>ress +} + +/// Address Doc +type Address { + street String +} diff --git a/prisma-fmt/tests/hover/scenarios/embedded_m2n_mongodb/result.json b/prisma-fmt/tests/hover/scenarios/embedded_m2n_mongodb/result.json new file mode 100644 index 000000000000..6c3e8a803c01 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/embedded_m2n_mongodb/result.json @@ -0,0 +1,6 @@ +{ + "contents": { + "kind": "markdown", + "value": "```prisma\nmodel Animals {\n\t...\n\tfamily Humans[] @relation(name: \"AnimalsToHumans\", fields: [humanIds], references: [id])\n}\n```\n___\nimplicit many-to-many\n___\n" + } +} \ No newline at end of file diff --git a/prisma-fmt/tests/hover/scenarios/embedded_m2n_mongodb/schema.prisma b/prisma-fmt/tests/hover/scenarios/embedded_m2n_mongodb/schema.prisma new file mode 100644 index 000000000000..0a3827e1490c --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/embedded_m2n_mongodb/schema.prisma @@ -0,0 +1,16 @@ +datasource db { + provider = "mongodb" + url = env("DATABASE_URL") +} + +model Humans { + id String @id @default(auto()) @map("_id") @db.ObjectId + animalIds String[] @db.ObjectId + family An<|>imals[] @relation(fields: [animalIds], references: [id]) +} + +model Animals { + id String @id @default(auto()) @map("_id") @db.ObjectId + humanIds String[] @db.ObjectId + family Humans[] @relation(fields: [humanIds], references: [id]) +} diff --git a/prisma-fmt/tests/hover/scenarios/enum_from_block_name/result.json b/prisma-fmt/tests/hover/scenarios/enum_from_block_name/result.json new file mode 100644 index 000000000000..7d48788f415c --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/enum_from_block_name/result.json @@ -0,0 +1,6 @@ +{ + "contents": { + "kind": "markdown", + "value": "```prisma\nenum Poly {}\n```\n___\nThis is doc for B" + } +} \ No newline at end of file diff --git a/prisma-fmt/tests/hover/scenarios/enum_from_block_name/schema.prisma b/prisma-fmt/tests/hover/scenarios/enum_from_block_name/schema.prisma new file mode 100644 index 000000000000..a0e46e50934f --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/enum_from_block_name/schema.prisma @@ -0,0 +1,20 @@ +generator js { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgres" + url = env("DATABASE_URL") +} + +model ModelNameA { + id Int @id + + poly Poly +} + +/// This is doc for B +enum Po<|>ly { +Am +Nesia +} diff --git a/prisma-fmt/tests/hover/scenarios/enum_from_field_type/result.json b/prisma-fmt/tests/hover/scenarios/enum_from_field_type/result.json new file mode 100644 index 000000000000..a072fe175d82 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/enum_from_field_type/result.json @@ -0,0 +1,6 @@ +{ + "contents": { + "kind": "markdown", + "value": "```prisma\nenum Animal {}\n```\n___\n" + } +} \ No newline at end of file diff --git a/prisma-fmt/tests/hover/scenarios/enum_from_field_type/schema.prisma b/prisma-fmt/tests/hover/scenarios/enum_from_field_type/schema.prisma new file mode 100644 index 000000000000..8f1b6cfdc826 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/enum_from_field_type/schema.prisma @@ -0,0 +1,16 @@ +datasource db { + provider = "mongodb" + url = env("DATABASE_URL") +} + +model User { + id String @id @map("_id") + pet Ani<|>mal +} + +// Animal Doc +enum Animal { + REDPANDA + CAT + DOG +} diff --git a/prisma-fmt/tests/hover/scenarios/field_from_composite_field_name/result.json b/prisma-fmt/tests/hover/scenarios/field_from_composite_field_name/result.json new file mode 100644 index 000000000000..5fa4df924f54 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/field_from_composite_field_name/result.json @@ -0,0 +1,6 @@ +{ + "contents": { + "kind": "markdown", + "value": "```prisma\nveryImportantField\n```\n___\nvery important documentation" + } +} \ No newline at end of file diff --git a/prisma-fmt/tests/hover/scenarios/field_from_composite_field_name/schema.prisma b/prisma-fmt/tests/hover/scenarios/field_from_composite_field_name/schema.prisma new file mode 100644 index 000000000000..ead9dd9ef278 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/field_from_composite_field_name/schema.prisma @@ -0,0 +1,15 @@ +generator js { + provider = "prisma-client-js" +} + +datasource db { + provider = "mongodb" + url = env("DATABASE_URL") +} + +type ModelNameA { + likesRedPandas Boolean + + /// very important documentation + veryImpor<|>tantField DateTime +} diff --git a/prisma-fmt/tests/hover/scenarios/field_from_model_field_name/result.json b/prisma-fmt/tests/hover/scenarios/field_from_model_field_name/result.json new file mode 100644 index 000000000000..5fa4df924f54 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/field_from_model_field_name/result.json @@ -0,0 +1,6 @@ +{ + "contents": { + "kind": "markdown", + "value": "```prisma\nveryImportantField\n```\n___\nvery important documentation" + } +} \ No newline at end of file diff --git a/prisma-fmt/tests/hover/scenarios/field_from_model_field_name/schema.prisma b/prisma-fmt/tests/hover/scenarios/field_from_model_field_name/schema.prisma new file mode 100644 index 000000000000..f05c1b1e48d5 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/field_from_model_field_name/schema.prisma @@ -0,0 +1,15 @@ +generator js { + provider = "prisma-client-js" +} + +datasource db { + provider = "mongodb" + url = env("DATABASE_URL") +} + +model ModelNameA { + id String @id @map("_id") + + /// very important documentation + veryImpor<|>tantField DateTime +} diff --git a/prisma-fmt/tests/hover/scenarios/model_from_block_name/result.json b/prisma-fmt/tests/hover/scenarios/model_from_block_name/result.json new file mode 100644 index 000000000000..7f77aa514659 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/model_from_block_name/result.json @@ -0,0 +1,6 @@ +{ + "contents": { + "kind": "markdown", + "value": "```prisma\nmodel ModelNameB {}\n```\n___\nThis is doc for B" + } +} \ No newline at end of file diff --git a/prisma-fmt/tests/hover/scenarios/model_from_block_name/schema.prisma b/prisma-fmt/tests/hover/scenarios/model_from_block_name/schema.prisma new file mode 100644 index 000000000000..632ac7e0ce48 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/model_from_block_name/schema.prisma @@ -0,0 +1,20 @@ +generator js { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgres" + url = env("DATABASE_URL") +} + +model ModelNameA { + id Int @id + bId Int + val ModelNameB @relation(fields: [bId], references: [id]) +} + +/// This is doc for B +model Model<|>NameB { +id Int @id +A ModelNameA[] +} diff --git a/prisma-fmt/tests/hover/scenarios/model_from_model_type_includes_broken_relations/result.json b/prisma-fmt/tests/hover/scenarios/model_from_model_type_includes_broken_relations/result.json new file mode 100644 index 000000000000..f99f30abaf57 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/model_from_model_type_includes_broken_relations/result.json @@ -0,0 +1,6 @@ +{ + "contents": { + "kind": "markdown", + "value": "```prisma\nmodel Post {\n\t...\n\tUser User? @relation(name: \"PostToUser\", fields: [userId], references: [id])\n}\n```\n___\none-to-many\n___\n" + } +} \ No newline at end of file diff --git a/prisma-fmt/tests/hover/scenarios/model_from_model_type_includes_broken_relations/schema.prisma b/prisma-fmt/tests/hover/scenarios/model_from_model_type_includes_broken_relations/schema.prisma new file mode 100644 index 000000000000..2b9d835c2b50 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/model_from_model_type_includes_broken_relations/schema.prisma @@ -0,0 +1,26 @@ +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model User { + id Int @id + + posts Po<|>st[] +} + +model Post { + id Int @id + + content String + + userId Int? + User User? @relation(fields: [userId], references: [id]) +} + +model interm { + id Int @id + + forumId Int + forum Forum @relation(fields: [forumId], references: [id]) +} diff --git a/prisma-fmt/tests/hover/scenarios/model_from_model_type_on_broken_relations/result.json b/prisma-fmt/tests/hover/scenarios/model_from_model_type_on_broken_relations/result.json new file mode 100644 index 000000000000..ec747fa47ddb --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/model_from_model_type_on_broken_relations/result.json @@ -0,0 +1 @@ +null \ No newline at end of file diff --git a/prisma-fmt/tests/hover/scenarios/model_from_model_type_on_broken_relations/schema.prisma b/prisma-fmt/tests/hover/scenarios/model_from_model_type_on_broken_relations/schema.prisma new file mode 100644 index 000000000000..bfd01e22c868 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/model_from_model_type_on_broken_relations/schema.prisma @@ -0,0 +1,12 @@ +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model interm { + id Int @id + + forumId Int + forum For<|>um @relation(fields: [forumId], references: [id]) +} + diff --git a/prisma-fmt/tests/hover/scenarios/model_from_view_type/a.prisma b/prisma-fmt/tests/hover/scenarios/model_from_view_type/a.prisma new file mode 100644 index 000000000000..28c6cb9d2695 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/model_from_view_type/a.prisma @@ -0,0 +1,6 @@ +/// This is doc for A +model ModelNameA { + id Int @id + bId Int + val ModelNameB @relation(fields: [bId], references: [id]) +} diff --git a/prisma-fmt/tests/hover/scenarios/model_from_view_type/b.prisma b/prisma-fmt/tests/hover/scenarios/model_from_view_type/b.prisma new file mode 100644 index 000000000000..934a52fcf33d --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/model_from_view_type/b.prisma @@ -0,0 +1,5 @@ +/// This is doc for B +view ModelNameB { + id Int @id + A ModelNa<|>meA[] +} diff --git a/prisma-fmt/tests/hover/scenarios/model_from_view_type/config.prisma b/prisma-fmt/tests/hover/scenarios/model_from_view_type/config.prisma new file mode 100644 index 000000000000..654be6eef819 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/model_from_view_type/config.prisma @@ -0,0 +1,9 @@ +generator js { + provider = "prisma-client-js" + previewFeatures = ["views", "prismaSchemaFolder"] +} + +datasource db { + provider = "postgres" + url = env("DATABASE_URL") +} diff --git a/prisma-fmt/tests/hover/scenarios/model_from_view_type/result.json b/prisma-fmt/tests/hover/scenarios/model_from_view_type/result.json new file mode 100644 index 000000000000..fd05a78439e1 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/model_from_view_type/result.json @@ -0,0 +1,6 @@ +{ + "contents": { + "kind": "markdown", + "value": "```prisma\nmodel ModelNameA {\n\t...\n\tval ModelNameB @relation(name: \"ModelNameAToModelNameB\", fields: [bId], references: [id])\n}\n```\n___\none-to-many\n___\nThis is doc for A" + } +} \ No newline at end of file diff --git a/prisma-fmt/tests/hover/scenarios/one_to_many_self_relation/result.json b/prisma-fmt/tests/hover/scenarios/one_to_many_self_relation/result.json new file mode 100644 index 000000000000..5663663e4274 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/one_to_many_self_relation/result.json @@ -0,0 +1,6 @@ +{ + "contents": { + "kind": "markdown", + "value": "```prisma\nmodel Bee {\n\t...\n\tB Bee? @relation(name: \"bees\", fields: [bId], references: [id])\n}\n```\n___\none-to-many on self\n___\n" + } +} \ No newline at end of file diff --git a/prisma-fmt/tests/hover/scenarios/one_to_many_self_relation/schema.prisma b/prisma-fmt/tests/hover/scenarios/one_to_many_self_relation/schema.prisma new file mode 100644 index 000000000000..f6b52dbe8d81 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/one_to_many_self_relation/schema.prisma @@ -0,0 +1,6 @@ +model Bee { + id Int @id + bees Be<|>e[] @relation(name: "bees") + B Bee? @relation(name: "bees", fields: [bId], references: [id]) + bId Int? +} diff --git a/prisma-fmt/tests/hover/scenarios/value_from_enum_value_name/result.json b/prisma-fmt/tests/hover/scenarios/value_from_enum_value_name/result.json new file mode 100644 index 000000000000..2f8ef36c7f24 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/value_from_enum_value_name/result.json @@ -0,0 +1,6 @@ +{ + "contents": { + "kind": "markdown", + "value": "```prisma\nRedPanda\n```\n___\nRedpandas are super cute." + } +} \ No newline at end of file diff --git a/prisma-fmt/tests/hover/scenarios/value_from_enum_value_name/schema.prisma b/prisma-fmt/tests/hover/scenarios/value_from_enum_value_name/schema.prisma new file mode 100644 index 000000000000..8b339228b780 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/value_from_enum_value_name/schema.prisma @@ -0,0 +1,15 @@ +generator js { + provider = "prisma-client-js" +} + +datasource db { + provider = "mongodb" + url = env("DATABASE_URL") +} + +/// enum doc +enum Pet { + /// Redpandas are super cute. + RedP<|>anda + Cat +} diff --git a/prisma-fmt/tests/hover/test_api.rs b/prisma-fmt/tests/hover/test_api.rs new file mode 100644 index 000000000000..598b08f8ead1 --- /dev/null +++ b/prisma-fmt/tests/hover/test_api.rs @@ -0,0 +1,144 @@ +use crate::helpers::load_schema_files; +use once_cell::sync::Lazy; +use std::{fmt::Write as _, io::Write as _}; + +const CURSOR_MARKER: &str = "<|>"; +const SCENARIOS_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/hover/scenarios"); +static UPDATE_EXPECT: Lazy = Lazy::new(|| std::env::var("UPDATE_EXPECT").is_ok()); + +pub(crate) fn test_scenario(scenario_name: &str) { + let mut path = String::with_capacity(SCENARIOS_PATH.len() + 12); + + let schema_files = { + write!(path, "{SCENARIOS_PATH}/{scenario_name}").unwrap(); + load_schema_files(&path) + }; + + path.clear(); + write!(path, "{SCENARIOS_PATH}/{scenario_name}/result.json").unwrap(); + let expected_result = std::fs::read_to_string(&path).unwrap_or_else(|_| String::new()); + + let (initiating_file_uri, cursor_position, schema_files) = take_cursor(schema_files); + + let params = lsp_types::HoverParams { + text_document_position_params: lsp_types::TextDocumentPositionParams { + text_document: lsp_types::TextDocumentIdentifier { + uri: initiating_file_uri.parse().unwrap(), + }, + position: cursor_position, + }, + work_done_progress_params: lsp_types::WorkDoneProgressParams { work_done_token: None }, + }; + + let result = prisma_fmt::hover( + serde_json::to_string_pretty(&schema_files).unwrap(), + &serde_json::to_string_pretty(¶ms).unwrap(), + ); + + // Prettify the JSON + let result = + serde_json::to_string_pretty(&serde_json::from_str::>(&result).unwrap()).unwrap(); + + if *UPDATE_EXPECT { + let mut file = std::fs::File::create(&path).unwrap(); // truncate + file.write_all(result.as_bytes()).unwrap(); + } else if expected_result != result { + let chunks = dissimilar::diff(&expected_result, &result); + panic!( + r#" +Snapshot comparison failed. Run the test again with UPDATE_EXPECT=1 in the environment to update the snapshot. + +===== EXPECTED ==== +{} +====== FOUND ====== +{} +======= DIFF ====== +{} +"#, + expected_result, + result, + format_chunks(chunks), + ); + } +} + +fn format_chunks(chunks: Vec) -> String { + let mut buf = String::new(); + for chunk in chunks { + let formatted = match chunk { + dissimilar::Chunk::Equal(text) => text.into(), + dissimilar::Chunk::Delete(text) => format!("\x1b[41m{text}\x1b[0m"), + dissimilar::Chunk::Insert(text) => format!("\x1b[42m{text}\x1b[0m"), + }; + buf.push_str(&formatted); + } + buf +} + +fn take_cursor(schema_files: Vec<(String, String)>) -> (String, lsp_types::Position, Vec<(String, String)>) { + let mut result = Vec::with_capacity(schema_files.len()); + let mut file_and_pos = None; + for (file_name, content) in schema_files { + if let Some((pos, without_cursor)) = take_cursor_one(&content) { + file_and_pos = Some((file_name.clone(), pos)); + result.push((file_name, without_cursor)); + } else { + result.push((file_name, content)); + } + } + + let (file_name, position) = file_and_pos.expect("Could not find a cursor in any of the schema files"); + + (file_name, position, result) +} + +fn take_cursor_one(schema: &str) -> Option<(lsp_types::Position, String)> { + let mut schema_without_cursor = String::with_capacity(schema.len() - 3); + let mut cursor_position = lsp_types::Position { character: 0, line: 0 }; + let mut cursor_found = false; + for line in schema.lines() { + if !cursor_found { + if let Some(pos) = line.find(CURSOR_MARKER) { + cursor_position.character = pos as u32; + cursor_found = true; + schema_without_cursor.push_str(&line[..pos]); + schema_without_cursor.push_str(&line[pos + 3..]); + schema_without_cursor.push('\n'); + } else { + schema_without_cursor.push_str(line); + schema_without_cursor.push('\n'); + cursor_position.line += 1; + } + } else { + schema_without_cursor.push_str(line); + schema_without_cursor.push('\n'); + } + } + + if !cursor_found { + return None; + } + // remove extra newline + schema_without_cursor.truncate(schema_without_cursor.len() - 1); + + Some((cursor_position, schema_without_cursor)) +} + +#[test] +fn take_cursor_works() { + let schema = r#" + model Test { + id Int @id @map(<|>) + } + "#; + let expected_schema = r#" + model Test { + id Int @id @map() + } + "#; + + let (pos, schema) = take_cursor_one(schema).unwrap(); + assert_eq!(pos.line, 2); + assert_eq!(pos.character, 28); + assert_eq!(schema, expected_schema); +} diff --git a/prisma-fmt/tests/hover/tests.rs b/prisma-fmt/tests/hover/tests.rs new file mode 100644 index 000000000000..2001dfec5b54 --- /dev/null +++ b/prisma-fmt/tests/hover/tests.rs @@ -0,0 +1,29 @@ +use super::test_api::test_scenario; + +macro_rules! scenarios { + ($($scenario_name:ident)+) => { + $( + #[test] + fn $scenario_name() { + test_scenario(stringify!($scenario_name)) + } + )* + } +} + +scenarios! { + composite_from_block_name + composite_from_field_type + embedded_m2n_mongodb + enum_from_block_name + enum_from_field_type + field_from_composite_field_name + field_from_model_field_name + model_from_block_name + model_from_model_type_includes_broken_relations + model_from_model_type_on_broken_relations + model_from_view_type + one_to_many_self_relation + value_from_enum_value_name + +} diff --git a/prisma-fmt/tests/hover_tests.rs b/prisma-fmt/tests/hover_tests.rs new file mode 100644 index 000000000000..cf416f3a1b37 --- /dev/null +++ b/prisma-fmt/tests/hover_tests.rs @@ -0,0 +1,2 @@ +mod helpers; +mod hover; diff --git a/prisma-schema-wasm/src/lib.rs b/prisma-schema-wasm/src/lib.rs index 36a2c9d353ff..8ef24cbcdfb3 100644 --- a/prisma-schema-wasm/src/lib.rs +++ b/prisma-schema-wasm/src/lib.rs @@ -123,6 +123,15 @@ pub fn references(schema: String, params: String) -> String { prisma_fmt::references(schema, ¶ms) } +/// This api is modelled on an LSP [hover request](https://github.com/microsoft/language-server-protocol/blob/gh-pages/_specifications/specification-3-16.md#hover-request-leftwards_arrow_with_hook). +/// Input and output are both JSON, the request being a `HoverParams` object +/// and the response being a `Hover` object. +#[wasm_bindgen] +pub fn hover(schema_files: String, params: String) -> String { + register_panic_hook(); + prisma_fmt::hover(schema_files, ¶ms) +} + /// Trigger a panic inside the wasm module. This is only useful in development for testing panic /// handling. #[wasm_bindgen] diff --git a/psl/parser-database/src/attributes.rs b/psl/parser-database/src/attributes.rs index 0d0bbfe786d3..b17c3d1a6676 100644 --- a/psl/parser-database/src/attributes.rs +++ b/psl/parser-database/src/attributes.rs @@ -45,7 +45,12 @@ fn resolve_composite_type_attributes<'db>( ctx: &mut Context<'db>, ) { for (field_id, field) in ct.iter_fields() { - let CompositeTypeField { r#type, .. } = ctx.types.composite_type_fields[&(ctid, field_id)]; + let CompositeTypeField { r#type, .. } = + if let Some(val) = ctx.types.composite_type_fields.get(&(ctid, field_id)) { + val.clone() + } else { + continue; + }; ctx.visit_attributes((ctid.0, (ctid.1, field_id))); @@ -81,14 +86,13 @@ fn resolve_composite_type_attributes<'db>( fn resolve_enum_attributes<'db>(enum_id: crate::EnumId, ast_enum: &'db ast::Enum, ctx: &mut Context<'db>) { let mut enum_attributes = EnumAttributes::default(); - for value_idx in 0..ast_enum.values.len() { - ctx.visit_attributes((enum_id.0, (enum_id.1, value_idx as u32))); + for (value_id, _) in ast_enum.iter_values() { + ctx.visit_attributes((enum_id.0, (enum_id.1, value_id))); // @map if ctx.visit_optional_single_attr("map") { if let Some(mapped_name) = map::visit_map_attribute(ctx) { - enum_attributes.mapped_values.insert(value_idx as u32, mapped_name); - ctx.mapped_enum_value_names - .insert((enum_id, mapped_name), value_idx as u32); + enum_attributes.mapped_values.insert(value_id, mapped_name); + ctx.mapped_enum_value_names.insert((enum_id, mapped_name), value_id); } ctx.validate_visited_arguments(); } @@ -629,15 +633,16 @@ fn common_index_validations( let mut suggested_fields = Vec::new(); for (_, field_id) in &relation_fields { - let fields = ctx + let Some(rf) = ctx .types .range_model_relation_fields(model_id) .find(|(_, rf)| rf.field_id == *field_id) - .unwrap() - .1 - .fields - .iter() - .flatten(); + else { + continue; + }; + + let fields = rf.1.fields.iter().flatten(); + for underlying_field in fields { let ScalarField { model_id, field_id, .. } = ctx.types[*underlying_field]; suggested_fields.push(ctx.asts[model_id][field_id].name()); @@ -1097,25 +1102,3 @@ fn validate_clustering_setting(ctx: &mut Context<'_>) -> Option { ctx.visit_optional_arg("clustered") .and_then(|sort| coerce::boolean(sort, ctx.diagnostics)) } - -/// Create the default values of [`ModelAttributes`] and [`EnumAttributes`] for each model and enum -/// in the AST to ensure [`crate::walkers::ModelWalker`] and [`crate::walkers::EnumWalker`] can -/// access their corresponding entries in the attributes map in the database even in the presence -/// of name and type resolution errors. This is useful for the language tools. -pub(super) fn create_default_attributes(ctx: &mut Context<'_>) { - for ((file_id, top), _) in ctx.iter_tops() { - match top { - ast::TopId::Model(model_id) => { - ctx.types - .model_attributes - .insert((file_id, model_id), ModelAttributes::default()); - } - ast::TopId::Enum(enum_id) => { - ctx.types - .enum_attributes - .insert((file_id, enum_id), EnumAttributes::default()); - } - _ => (), - } - } -} diff --git a/psl/parser-database/src/context.rs b/psl/parser-database/src/context.rs index 6d4d72239824..fb62d8a8b26a 100644 --- a/psl/parser-database/src/context.rs +++ b/psl/parser-database/src/context.rs @@ -5,7 +5,7 @@ use crate::{ ast, interner::StringInterner, names::Names, relations::Relations, types::Types, DatamodelError, Diagnostics, InFile, StringId, }; -use schema_ast::ast::{Expression, WithName}; +use schema_ast::ast::{EnumValueId, Expression, WithName}; use std::collections::{HashMap, HashSet}; /// Validation context. This is an implementation detail of ParserDatabase. It @@ -33,7 +33,7 @@ pub(crate) struct Context<'db> { pub(super) mapped_model_scalar_field_names: HashMap<(crate::ModelId, StringId), ast::FieldId>, pub(super) mapped_composite_type_names: HashMap<(crate::CompositeTypeId, StringId), ast::FieldId>, pub(super) mapped_enum_names: HashMap, - pub(super) mapped_enum_value_names: HashMap<(crate::EnumId, StringId), u32>, + pub(super) mapped_enum_value_names: HashMap<(crate::EnumId, StringId), EnumValueId>, } impl<'db> Context<'db> { diff --git a/psl/parser-database/src/lib.rs b/psl/parser-database/src/lib.rs index 5764248eff36..5ada8cebb961 100644 --- a/psl/parser-database/src/lib.rs +++ b/psl/parser-database/src/lib.rs @@ -107,35 +107,9 @@ impl ParserDatabase { // First pass: resolve names. names::resolve_names(&mut ctx); - // Return early on name resolution errors. - if ctx.diagnostics.has_errors() { - attributes::create_default_attributes(&mut ctx); - - return ParserDatabase { - asts, - interner, - names, - types, - relations, - }; - } - // Second pass: resolve top-level items and field types. types::resolve_types(&mut ctx); - // Return early on type resolution errors. - if ctx.diagnostics.has_errors() { - attributes::create_default_attributes(&mut ctx); - - return ParserDatabase { - asts, - interner, - names, - types, - relations, - }; - } - // Third pass: validate model and field attributes. All these // validations should be _order independent_ and only rely on // information from previous steps, not from other attributes. diff --git a/psl/parser-database/src/relations.rs b/psl/parser-database/src/relations.rs index 0c1e0a454c69..cbc2174f8103 100644 --- a/psl/parser-database/src/relations.rs +++ b/psl/parser-database/src/relations.rs @@ -195,6 +195,10 @@ impl Relation { matches!(self.attributes, RelationAttributes::ImplicitManyToMany { .. }) } + pub(crate) fn is_two_way_embedded_many_to_many(&self) -> bool { + matches!(self.attributes, RelationAttributes::TwoWayEmbeddedManyToMany { .. }) + } + pub(crate) fn as_complete_fields(&self) -> Option<(RelationFieldId, RelationFieldId)> { match &self.attributes { RelationAttributes::ImplicitManyToMany { field_a, field_b } => Some((*field_a, *field_b)), @@ -206,10 +210,6 @@ impl Relation { _ => None, } } - - pub(crate) fn is_two_way_embedded_many_to_many(&self) -> bool { - matches!(self.attributes, RelationAttributes::TwoWayEmbeddedManyToMany { .. }) - } } // Implementation detail for this module. Should stay private. diff --git a/psl/parser-database/src/types.rs b/psl/parser-database/src/types.rs index c7626e08649d..7d8a0c6a949f 100644 --- a/psl/parser-database/src/types.rs +++ b/psl/parser-database/src/types.rs @@ -4,7 +4,7 @@ use crate::{context::Context, interner::StringId, walkers::IndexFieldWalker, Dat use either::Either; use enumflags2::bitflags; use rustc_hash::FxHashMap as HashMap; -use schema_ast::ast::{self, WithName}; +use schema_ast::ast::{self, EnumValueId, WithName}; use std::{collections::BTreeMap, fmt}; pub(super) fn resolve_types(ctx: &mut Context<'_>) { @@ -21,6 +21,12 @@ pub(super) fn resolve_types(ctx: &mut Context<'_>) { } } +pub enum RefinedFieldVariant { + Relation(RelationFieldId), + Scalar(ScalarFieldId), + Unknown, +} + #[derive(Debug, Default)] pub(super) struct Types { pub(super) composite_type_fields: BTreeMap<(crate::CompositeTypeId, ast::FieldId), CompositeTypeField>, @@ -92,16 +98,16 @@ impl Types { .map(move |(idx, rf)| (RelationFieldId((first_relation_field_idx + idx) as u32), rf)) } - pub(super) fn refine_field(&self, id: (crate::ModelId, ast::FieldId)) -> Either { + pub(super) fn refine_field(&self, id: (crate::ModelId, ast::FieldId)) -> RefinedFieldVariant { self.relation_fields .binary_search_by_key(&id, |rf| (rf.model_id, rf.field_id)) - .map(|idx| Either::Left(RelationFieldId(idx as u32))) + .map(|idx| RefinedFieldVariant::Relation(RelationFieldId(idx as u32))) .or_else(|_| { self.scalar_fields .binary_search_by_key(&id, |sf| (sf.model_id, sf.field_id)) - .map(|id| Either::Right(ScalarFieldId(id as u32))) + .map(|id| RefinedFieldVariant::Scalar(ScalarFieldId(id as u32))) }) - .expect("expected field to be either scalar or relation field") + .unwrap_or(RefinedFieldVariant::Unknown) } pub(super) fn push_relation_field(&mut self, relation_field: RelationField) -> RelationFieldId { @@ -623,7 +629,7 @@ pub struct FieldWithArgs { pub(super) struct EnumAttributes { pub(super) mapped_name: Option, /// @map on enum values. - pub(super) mapped_values: HashMap, + pub(super) mapped_values: HashMap, /// ```ignore /// @@schema("public") /// ^^^^^^^^ diff --git a/psl/parser-database/src/walkers/composite_type.rs b/psl/parser-database/src/walkers/composite_type.rs index d6c93f125284..bf12e30d7c66 100644 --- a/psl/parser-database/src/walkers/composite_type.rs +++ b/psl/parser-database/src/walkers/composite_type.rs @@ -5,6 +5,8 @@ use crate::{ }; use diagnostics::Span; +use super::EnumWalker; + /// A composite type, introduced with the `type` keyword in the schema. /// /// Example: @@ -50,6 +52,11 @@ impl<'db> CompositeTypeWalker<'db> { self.ast_composite_type().name() } + /// Returns a specific field from the model. + pub fn field(&self, field_id: ast::FieldId) -> CompositeTypeFieldWalker<'db> { + self.walk((self.id, field_id)) + } + /// Iterator over all the fields of the composite type. pub fn fields(self) -> impl ExactSizeIterator> + Clone { self.ast_composite_type() @@ -111,6 +118,16 @@ impl<'db> CompositeTypeFieldWalker<'db> { self.ast_field().arity } + /// Is this field's type an enum? If yes, walk the enum. + pub fn field_type_as_enum(self) -> Option> { + self.r#type().as_enum().map(|id| self.db.walk(id)) + } + + /// Is this field's type a composite type? If yes, walk the composite type. + pub fn field_type_as_composite_type(self) -> Option> { + self.r#type().as_composite_type().map(|id| self.db.walk(id)) + } + /// The type of the field, e.g. `String` in `streetName String?`. pub fn r#type(self) -> ScalarFieldType { self.field().r#type diff --git a/psl/parser-database/src/walkers/enum.rs b/psl/parser-database/src/walkers/enum.rs index 8059ad73e5d3..659a713f1ed8 100644 --- a/psl/parser-database/src/walkers/enum.rs +++ b/psl/parser-database/src/walkers/enum.rs @@ -7,7 +7,7 @@ use crate::{ /// An `enum` declaration in the schema. pub type EnumWalker<'db> = Walker<'db, crate::EnumId>; /// One value in an `enum` declaration in the schema. -pub type EnumValueWalker<'db> = Walker<'db, (crate::EnumId, usize)>; +pub type EnumValueWalker<'db> = Walker<'db, (crate::EnumId, ast::EnumValueId)>; impl<'db> EnumWalker<'db> { fn attributes(self) -> &'db types::EnumAttributes { @@ -45,9 +45,16 @@ impl<'db> EnumWalker<'db> { self.attributes().mapped_name.map(|id| &self.db[id]) } + /// Returns the specific value from the model. + pub fn value(self, value_id: ast::EnumValueId) -> EnumValueWalker<'db> { + self.walk((self.id, value_id)) + } + /// The values of the enum. pub fn values(self) -> impl ExactSizeIterator> { - (0..self.ast_enum().values.len()).map(move |idx| self.walk((self.id, idx))) + self.ast_enum() + .iter_values() + .map(move |(value_id, _)| self.walk((self.id, value_id))) } /// How fields are indented in the enum. @@ -79,18 +86,19 @@ impl<'db> EnumWalker<'db> { } impl<'db> EnumValueWalker<'db> { - fn r#enum(self) -> EnumWalker<'db> { - self.walk(self.id.0) + /// The AST node. + pub fn ast_value(self) -> &'db ast::EnumValue { + &self.db.asts[self.id.0][self.id.1] } /// The enum documentation pub fn documentation(self) -> Option<&'db str> { - self.r#enum().ast_enum().values[self.id.1].documentation() + self.ast_value().documentation() } /// The name of the value. pub fn name(self) -> &'db str { - &self.r#enum().ast_enum().values[self.id.1].name.name + self.ast_value().name() } /// The database name of the enum. @@ -111,7 +119,7 @@ impl<'db> EnumValueWalker<'db> { pub fn mapped_name(self) -> Option<&'db str> { self.db.types.enum_attributes[&self.id.0] .mapped_values - .get(&(self.id.1 as u32)) + .get(&(self.id.1)) .map(|id| &self.db[*id]) } } diff --git a/psl/parser-database/src/walkers/field.rs b/psl/parser-database/src/walkers/field.rs index 87bea6560344..a27b094d4277 100644 --- a/psl/parser-database/src/walkers/field.rs +++ b/psl/parser-database/src/walkers/field.rs @@ -1,6 +1,6 @@ use super::{CompositeTypeFieldWalker, ModelWalker, RelationFieldWalker, ScalarFieldWalker, Walker}; use crate::{ - types::{RelationField, ScalarField}, + types::{RefinedFieldVariant, RelationField, ScalarField}, ScalarType, }; use schema_ast::ast; @@ -25,12 +25,20 @@ impl<'db> FieldWalker<'db> { } /// Find out which kind of field this is. - pub fn refine(self) -> RefinedFieldWalker<'db> { + /// Returns `None` if we encounter an unknown field. + pub fn refine(self) -> Option> { match self.db.types.refine_field(self.id) { - either::Either::Left(id) => RefinedFieldWalker::Relation(self.walk(id)), - either::Either::Right(id) => RefinedFieldWalker::Scalar(self.walk(id)), + RefinedFieldVariant::Relation(id) => Some(RefinedFieldWalker::Relation(self.walk(id))), + RefinedFieldVariant::Scalar(id) => Some(RefinedFieldWalker::Scalar(self.walk(id))), + RefinedFieldVariant::Unknown => None, } } + + /// Find out which kind of field this is. + /// ! Panics on unknown field, only to be used in query-engine where unknowns should not exist. + pub fn refine_known(self) -> RefinedFieldWalker<'db> { + self.refine().unwrap() + } } /// A field that has been identified as scalar field or relation field. diff --git a/psl/parser-database/src/walkers/model.rs b/psl/parser-database/src/walkers/model.rs index 088302095f3d..ea32bfe3acea 100644 --- a/psl/parser-database/src/walkers/model.rs +++ b/psl/parser-database/src/walkers/model.rs @@ -30,6 +30,11 @@ impl<'db> ModelWalker<'db> { self.id.0 } + /// Returns the specific field from the model. + pub fn field(&self, field_id: ast::FieldId) -> FieldWalker<'db> { + self.walk((self.id, field_id)) + } + /// Traverse the fields of the models in the order they were defined. pub fn fields(self) -> impl ExactSizeIterator> + Clone { self.ast_model() diff --git a/psl/parser-database/src/walkers/relation.rs b/psl/parser-database/src/walkers/relation.rs index 26e3ec61e052..6c017b1b2e2a 100644 --- a/psl/parser-database/src/walkers/relation.rs +++ b/psl/parser-database/src/walkers/relation.rs @@ -15,13 +15,13 @@ pub type RelationWalker<'db> = Walker<'db, RelationId>; impl<'db> RelationWalker<'db> { /// The models at each end of the relation. [model A, model B]. Can be the same model twice. pub fn models(self) -> [(FileId, ast::ModelId); 2] { - let rel = self.get(); + let rel = self.ast_relation(); [rel.model_a, rel.model_b] } /// The relation fields that define the relation. A then B. pub fn relation_fields(self) -> impl Iterator> { - let (a, b) = self.get().attributes.fields(); + let (a, b) = self.ast_relation().attributes.fields(); [a, b].into_iter().flatten().map(move |field| self.walk(field)) } @@ -38,16 +38,28 @@ impl<'db> RelationWalker<'db> { /// Is this a relation where both ends are the same model? pub fn is_self_relation(self) -> bool { - let r = self.get(); + let r = self.ast_relation(); r.model_a == r.model_b } + /// Gets relation kind + pub fn relation_kind(self) -> &'db str { + let r = self.ast_relation(); + + match r.attributes { + RelationAttributes::ImplicitManyToMany { .. } => "implicit many-to-many", + RelationAttributes::TwoWayEmbeddedManyToMany { .. } => "implicit many-to-many", + RelationAttributes::OneToOne(_) => "one-to-one", + RelationAttributes::OneToMany(_) => "one-to-many", + } + } + /// Converts the walker to either an implicit many to many, or a inline relation walker /// gathering 1:1 and 1:n relations. pub fn refine(self) -> RefinedRelationWalker<'db> { - if self.get().is_implicit_many_to_many() { + if self.ast_relation().is_implicit_many_to_many() { RefinedRelationWalker::ImplicitManyToMany(self.walk(ManyToManyRelationId(self.id))) - } else if self.get().is_two_way_embedded_many_to_many() { + } else if self.ast_relation().is_two_way_embedded_many_to_many() { RefinedRelationWalker::TwoWayEmbeddedManyToMany(TwoWayEmbeddedManyToManyRelationWalker(self)) } else { RefinedRelationWalker::Inline(InlineRelationWalker(self)) @@ -61,7 +73,7 @@ impl<'db> RelationWalker<'db> { /// // ^^^^^^^^^^^^^^^^^^^^^^^ /// ``` pub fn explicit_relation_name(self) -> Option<&'db str> { - self.get().relation_name.map(|string_id| &self.db[string_id]) + self.ast_relation().relation_name.map(|string_id| &self.db[string_id]) } /// The relation name, explicit or inferred. @@ -71,7 +83,7 @@ impl<'db> RelationWalker<'db> { /// ^^^^^^^^^^^ /// ``` pub fn relation_name(self) -> RelationName<'db> { - let relation = self.get(); + let relation = self.ast_relation(); relation .relation_name .map(|s| RelationName::Explicit(&self.db[s])) @@ -81,7 +93,7 @@ impl<'db> RelationWalker<'db> { } /// The relation attributes parsed from the AST. - fn get(self) -> &'db Relation { + fn ast_relation(self) -> &'db Relation { &self.db.relations[self.id] } } diff --git a/psl/parser-database/src/walkers/relation_field.rs b/psl/parser-database/src/walkers/relation_field.rs index 7f6b2e8037a4..5e387480d0b7 100644 --- a/psl/parser-database/src/walkers/relation_field.rs +++ b/psl/parser-database/src/walkers/relation_field.rs @@ -4,7 +4,11 @@ use crate::{ walkers::*, ReferentialAction, }; -use std::{borrow::Cow, fmt, hash::Hasher}; +use std::{ + borrow::Cow, + fmt::{self, Debug}, + hash::Hasher, +}; /// A relation field on a model in the schema. pub type RelationFieldWalker<'db> = Walker<'db, RelationFieldId>; @@ -97,7 +101,7 @@ impl<'db> RelationFieldWalker<'db> { self.db.walk(self.attributes().referenced_model) } - /// The fields in the `@relation(references: ...)` argument. + /// The fields in the `@relation(references: [...])` argument. pub fn referenced_fields(self) -> Option>> { self.attributes() .references @@ -154,7 +158,7 @@ impl<'db> RelationFieldWalker<'db> { self.fields() } - /// The fields in the `fields: [...]` argument in the forward relation field. + /// The fields in the `@relation(fields: [...])` argument in the forward relation field. pub fn fields(self) -> Option> + Clone> { let attributes = &self.db.types[self.id]; attributes diff --git a/psl/parser-database/src/walkers/scalar_field.rs b/psl/parser-database/src/walkers/scalar_field.rs index 7a9a0984584a..69b9add31d16 100644 --- a/psl/parser-database/src/walkers/scalar_field.rs +++ b/psl/parser-database/src/walkers/scalar_field.rs @@ -1,3 +1,5 @@ +use std::fmt; + use crate::{ ast::{self, WithName}, types::{DefaultAttribute, FieldWithArgs, OperatorClassStore, ScalarField, ScalarType, SortOrder}, @@ -108,6 +110,11 @@ impl<'db> ScalarFieldWalker<'db> { self.scalar_field_type().as_enum().map(|id| self.db.walk(id)) } + /// Is this field's type a composite type? If yes, walk the composite type. + pub fn field_type_as_composite_type(self) -> Option> { + self.scalar_field_type().as_composite_type().map(|id| self.db.walk(id)) + } + /// The name in the `@map()` attribute. pub fn mapped_name(self) -> Option<&'db str> { self.attributes().mapped_name.map(|id| &self.db[id]) @@ -158,6 +165,12 @@ impl<'db> ScalarFieldWalker<'db> { } } +impl<'db> fmt::Display for ScalarFieldWalker<'db> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name()) + } +} + /// An `@default()` attribute on a field. #[derive(Clone, Copy)] pub struct DefaultValueWalker<'db> { diff --git a/psl/psl/tests/attributes/composite_index.rs b/psl/psl/tests/attributes/composite_index.rs index ca13e4a32105..a98887b26eec 100644 --- a/psl/psl/tests/attributes/composite_index.rs +++ b/psl/psl/tests/attributes/composite_index.rs @@ -533,6 +533,12 @@ fn pointing_to_a_non_existing_type() { 16 |  id Int @id @map("_id") 17 |  a C  |  + error: Error validating model "B": The index definition refers to the relation fields a. Index definitions must reference only scalar fields. + --> schema.prisma:19 +  |  + 18 |  + 19 |  @@index([a.field]) +  |  "#]]; expected.assert_eq(&error); diff --git a/psl/psl/tests/base/basic.rs b/psl/psl/tests/base/basic.rs index b82a427be650..1f81fce3bbaa 100644 --- a/psl/psl/tests/base/basic.rs +++ b/psl/psl/tests/base/basic.rs @@ -235,6 +235,12 @@ fn type_aliases_must_error() {  |   1 | type MyString = String @default("B")  |  + error: Type "MyString" is neither a built-in type, nor refers to another model, composite type, or enum. + --> schema.prisma:5 +  |  +  4 |  id Int @id +  5 |  val MyString +  |  "#]]; expectation.assert_eq(&error); diff --git a/psl/psl/tests/validation/composite_types/index_attributes_on_composite_types.prisma b/psl/psl/tests/validation/composite_types/index_attributes_on_composite_types.prisma index 0fcd7f8a981c..f8b637573f80 100644 --- a/psl/psl/tests/validation/composite_types/index_attributes_on_composite_types.prisma +++ b/psl/psl/tests/validation/composite_types/index_attributes_on_composite_types.prisma @@ -60,3 +60,15 @@ model B { // 15 |  @@index([other, field]) // 16 |  @@unique([content, rank]) //  |  +// error: Attribute not known: "@id". +// --> schema.prisma:7 +//  |  +//  6 | type A { +//  7 |  pk String @id +//  |  +// error: Attribute not known: "@unique". +// --> schema.prisma:8 +//  |  +//  7 |  pk String @id +//  8 |  field Int @unique +//  |  diff --git a/psl/psl/tests/validation/composite_types/relation_field_attribute_not_allowed.prisma b/psl/psl/tests/validation/composite_types/relation_field_attribute_not_allowed.prisma index 7d20fa73475e..2fc50789879b 100644 --- a/psl/psl/tests/validation/composite_types/relation_field_attribute_not_allowed.prisma +++ b/psl/psl/tests/validation/composite_types/relation_field_attribute_not_allowed.prisma @@ -24,3 +24,9 @@ model B { // 11 |  c C[] @relation("foo") // 12 | } //  |  +// error: Attribute not known: "@relation". +// --> schema.prisma:11 +//  |  +// 10 | type A { +// 11 |  c C[] @relation("foo") +//  |  diff --git a/psl/schema-ast/src/ast/attribute.rs b/psl/schema-ast/src/ast/attribute.rs index fbf508bfa473..f664e4da2c5e 100644 --- a/psl/schema-ast/src/ast/attribute.rs +++ b/psl/schema-ast/src/ast/attribute.rs @@ -1,4 +1,4 @@ -use super::{ArgumentsList, Identifier, Span, WithIdentifier, WithSpan}; +use super::{ArgumentsList, EnumValueId, Identifier, Span, WithIdentifier, WithSpan}; use std::ops::Index; /// An attribute (following `@` or `@@``) on a model, model field, enum, enum value or composite @@ -51,7 +51,7 @@ pub enum AttributeContainer { Model(super::ModelId), ModelField(super::ModelId, super::FieldId), Enum(super::EnumId), - EnumValue(super::EnumId, u32), + EnumValue(super::EnumId, super::EnumValueId), CompositeTypeField(super::CompositeTypeId, super::FieldId), } @@ -79,8 +79,8 @@ impl From<(super::CompositeTypeId, super::FieldId)> for AttributeContainer { } } -impl From<(super::EnumId, u32)> for AttributeContainer { - fn from((enm, val): (super::EnumId, u32)) -> Self { +impl From<(super::EnumId, EnumValueId)> for AttributeContainer { + fn from((enm, val): (super::EnumId, super::EnumValueId)) -> Self { Self::EnumValue(enm, val) } } @@ -103,7 +103,7 @@ impl Index for super::SchemaAst { AttributeContainer::Model(model_id) => &self[model_id].attributes, AttributeContainer::ModelField(model_id, field_id) => &self[model_id][field_id].attributes, AttributeContainer::Enum(enum_id) => &self[enum_id].attributes, - AttributeContainer::EnumValue(enum_id, value_idx) => &self[enum_id].values[value_idx as usize].attributes, + AttributeContainer::EnumValue(enum_id, value_idx) => &self[enum_id][value_idx].attributes, AttributeContainer::CompositeTypeField(ctid, field_id) => &self[ctid][field_id].attributes, } } diff --git a/psl/schema-ast/src/ast/enum.rs b/psl/schema-ast/src/ast/enum.rs index 6ef4e1326c96..990e599d0e7f 100644 --- a/psl/schema-ast/src/ast/enum.rs +++ b/psl/schema-ast/src/ast/enum.rs @@ -112,6 +112,12 @@ impl WithDocumentation for Enum { pub struct EnumValue { /// The name of the enum value as it will be exposed by the api. pub name: Identifier, + /// The attributes of this value. + /// + /// ```ignore + /// yellow @map("orange") + /// ^^^^^^^^^^^^^^ + /// ``` pub attributes: Vec, pub(crate) documentation: Option, /// The location of this enum value in the text representation. diff --git a/psl/schema-ast/src/ast/field.rs b/psl/schema-ast/src/ast/field.rs index 3e355ecc2b41..394381a2f3b1 100644 --- a/psl/schema-ast/src/ast/field.rs +++ b/psl/schema-ast/src/ast/field.rs @@ -1,3 +1,5 @@ +use std::fmt::Display; + use super::{ Attribute, Comment, Identifier, Span, WithAttributes, WithDocumentation, WithIdentifier, WithName, WithSpan, }; @@ -40,6 +42,20 @@ pub struct Field { pub(crate) span: Span, } +impl Display for Field { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let extension = if self.arity.is_list() { + "[]" + } else if self.arity.is_optional() { + "?" + } else { + "" + }; + + write!(f, "{} {}{}", self.name(), self.field_type.name(), extension) + } +} + impl Field { /// Finds the position span of the argument in the given field attribute. pub fn span_for_argument(&self, attribute: &str, argument: &str) -> Option { diff --git a/psl/schema-ast/src/ast/find_at_position.rs b/psl/schema-ast/src/ast/find_at_position.rs index b1a5c458bf79..c0201cf6c381 100644 --- a/psl/schema-ast/src/ast/find_at_position.rs +++ b/psl/schema-ast/src/ast/find_at_position.rs @@ -16,7 +16,7 @@ pub use field::FieldPosition; pub use generator::GeneratorPosition; pub use model::ModelPosition; pub use property::PropertyPosition; -pub use r#enum::EnumPosition; +pub use r#enum::{EnumPosition, EnumValuePosition}; use crate::ast::{self, top_idx_to_top_id, traits::*}; diff --git a/psl/schema-ast/src/ast/find_at_position/enum.rs b/psl/schema-ast/src/ast/find_at_position/enum.rs index 3138ef36e4c3..4749d4833a1b 100644 --- a/psl/schema-ast/src/ast/find_at_position/enum.rs +++ b/psl/schema-ast/src/ast/find_at_position/enum.rs @@ -63,6 +63,8 @@ impl<'ast> EnumPosition<'ast> { pub enum EnumValuePosition<'ast> { /// Nowhere specific inside the value Value, + /// In the name + Name(&'ast str), /// In an attribute. (name, idx, optional arg) /// In a value. /// ```prisma @@ -77,6 +79,9 @@ pub enum EnumValuePosition<'ast> { impl<'ast> EnumValuePosition<'ast> { fn new(value: &'ast ast::EnumValue, position: usize) -> EnumValuePosition<'ast> { + if value.name.span().contains(position) { + return EnumValuePosition::Name(value.name()); + } for (attr_idx, attr) in value.attributes.iter().enumerate() { if attr.span().contains(position) { // We can't go by Span::contains() because we also care about the empty space diff --git a/query-engine/dmmf/src/ast_builders/datamodel_ast_builder.rs b/query-engine/dmmf/src/ast_builders/datamodel_ast_builder.rs index c367695150f6..67b5417d4ab3 100644 --- a/query-engine/dmmf/src/ast_builders/datamodel_ast_builder.rs +++ b/query-engine/dmmf/src/ast_builders/datamodel_ast_builder.rs @@ -148,14 +148,14 @@ fn model_to_dmmf(model: walkers::ModelWalker<'_>) -> Model { } fn should_skip_model_field(field: &walkers::FieldWalker<'_>) -> bool { - match field.refine() { + match field.refine_known() { walkers::RefinedFieldWalker::Scalar(f) => f.is_ignored() || f.is_unsupported(), walkers::RefinedFieldWalker::Relation(f) => f.is_ignored(), } } fn field_to_dmmf(field: walkers::FieldWalker<'_>) -> Field { - match field.refine() { + match field.refine_known() { walkers::RefinedFieldWalker::Scalar(sf) => scalar_field_to_dmmf(sf), walkers::RefinedFieldWalker::Relation(rf) => relation_field_to_dmmf(rf), }