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(psl-core): implement parse_configuration_multi_file to correct prisma/prisma's getConfig implementation #4825

Merged
merged 5 commits into from May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 8 additions & 11 deletions prisma-fmt/src/get_config.rs
@@ -1,4 +1,4 @@
use psl::{Diagnostics, ValidatedSchema};
use psl::{parser_database::Files, Diagnostics};
use serde::Deserialize;
use serde_json::json;
use std::collections::HashMap;
Expand Down Expand Up @@ -43,26 +43,23 @@ pub(crate) fn get_config(params: &str) -> Result<String, String> {
}

fn get_config_impl(params: GetConfigParams) -> Result<serde_json::Value, GetConfigError> {
let mut schema = psl::validate_multi_file(params.prisma_schema.into());
if schema.diagnostics.has_errors() {
return Err(create_get_config_error(&schema, &schema.diagnostics));
}
let (files, mut config) =
psl::parse_configuration_multi_file(params.prisma_schema.into()).map_err(create_get_config_error)?;

if !params.ignore_env_var_errors {
let overrides: Vec<(_, _)> = params.datasource_overrides.into_iter().collect();
schema
.configuration
config
.resolve_datasource_urls_prisma_fmt(&overrides, |key| params.env.get(key).map(String::from))
.map_err(|diagnostics| create_get_config_error(&schema, &diagnostics))?;
.map_err(|diagnostics| create_get_config_error((files, diagnostics)))?;
}

Ok(psl::get_config(&schema.configuration))
Ok(psl::get_config(&config))
}

fn create_get_config_error(schema: &ValidatedSchema, diagnostics: &Diagnostics) -> GetConfigError {
fn create_get_config_error((files, diagnostics): (Files, Diagnostics)) -> GetConfigError {
use std::fmt::Write as _;

let mut rendered_diagnostics = schema.render_diagnostics(diagnostics);
let mut rendered_diagnostics = files.render_diagnostics(&diagnostics);
write!(
rendered_diagnostics,
"\nValidation Error Count: {}",
Expand Down
38 changes: 35 additions & 3 deletions psl/parser-database/src/files.rs
@@ -1,27 +1,59 @@
use crate::FileId;
use diagnostics::Diagnostics;
use schema_ast::ast;
use std::ops::Index;

/// The content is a list of (file path, file source text, file AST).
///
/// The file path can be anything, the PSL implementation will only use it to display the file name
/// in errors. For example, files can come from nested directories.
pub(crate) struct Files(pub(super) Vec<(String, schema_ast::SourceFile, ast::SchemaAst)>);
pub struct Files(pub Vec<(String, schema_ast::SourceFile, ast::SchemaAst)>);

impl Files {
pub(crate) fn iter(&self) -> impl Iterator<Item = (FileId, &String, &schema_ast::SourceFile, &ast::SchemaAst)> {
/// Create a new Files instance from multiple files.
pub fn new(files: Vec<(String, schema_ast::SourceFile)>, diagnostics: &mut Diagnostics) -> Self {
let asts = files
.into_iter()
.enumerate()
.map(|(file_idx, (path, source))| {
let id = FileId(file_idx as u32);
let ast = schema_ast::parse_schema(source.as_str(), diagnostics, id);
(path, source, ast)
})
.collect();
Self(asts)
}

/// Iterate all parsed files.
#[allow(clippy::should_implement_trait)]
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not implement trait instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because of

`impl Trait` in associated types is unstable
see issue #63063 <https://github.com/rust-lang/rust/issues/63063> for more information
add `#![feature(impl_trait_in_assoc_type)]` to the crate attributes to enable

which appears when trying to write e.g.

impl std::iter::IntoIterator for Files {
    type Item = (FileId, String, schema_ast::SourceFile, ast::SchemaAst);
    type IntoIter = impl Iterator<Item = (FileId, String, schema_ast::SourceFile, ast::SchemaAst)>;

    fn into_iter(self) -> Self::IntoIter {
        self.0
            .into_iter()
            .enumerate()
            .map(|(idx, (path, contents, ast))| (FileId(idx as u32), path, contents, ast))
    }
}

Also, iter and into_iter were previously defined like this by Tom, and I'd assume the reason he didn't implement the traits is the same one as mine. Do you have any explicit recommendation to work around this, or is it ok to leave the clippy warning suppressors as committed?

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, i see. LGTM with no comments then

pub fn iter(&self) -> impl Iterator<Item = (FileId, &String, &schema_ast::SourceFile, &ast::SchemaAst)> {
self.0
.iter()
.enumerate()
.map(|(idx, (path, contents, ast))| (FileId(idx as u32), path, contents, ast))
}

pub(crate) fn into_iter(self) -> impl Iterator<Item = (FileId, String, schema_ast::SourceFile, ast::SchemaAst)> {
/// Iterate all parsed files, consuming the parser database.
#[allow(clippy::should_implement_trait)]
pub fn into_iter(self) -> impl Iterator<Item = (FileId, String, schema_ast::SourceFile, ast::SchemaAst)> {
self.0
.into_iter()
.enumerate()
.map(|(idx, (path, contents, ast))| (FileId(idx as u32), path, contents, ast))
}

/// Render the given diagnostics (warnings + errors) into a String.
/// This method is multi-file aware.
pub fn render_diagnostics(&self, diagnostics: &Diagnostics) -> String {
let mut out = Vec::new();

for error in diagnostics.errors() {
let (file_name, source, _) = &self[error.span().file_id];
error.pretty_print(&mut out, file_name, source.as_str()).unwrap();
}

String::from_utf8(out).unwrap()
}
}

impl Index<crate::FileId> for Files {
Expand Down
25 changes: 11 additions & 14 deletions psl/parser-database/src/lib.rs
Expand Up @@ -38,21 +38,21 @@ mod names;
mod relations;
mod types;

use self::{context::Context, interner::StringId, relations::Relations, types::Types};
pub use coerce_expression::{coerce, coerce_array, coerce_opt};
pub use diagnostics::FileId;
use diagnostics::{DatamodelError, Diagnostics};
pub use files::Files;
pub use ids::*;
pub use names::is_reserved_type_name;
use names::Names;
pub use relations::{ManyToManyRelationId, ReferentialAction, RelationId};
pub use schema_ast::{ast, SourceFile};
pub use types::{
IndexAlgorithm, IndexFieldPath, IndexType, OperatorClass, RelationFieldId, ScalarFieldId, ScalarFieldType,
ScalarType, SortOrder,
};

use self::{context::Context, files::Files, interner::StringId, relations::Relations, types::Types};
use diagnostics::{DatamodelError, Diagnostics};
use names::Names;

/// ParserDatabase is a container for a Schema AST, together with information
/// gathered during schema validation. Each validation step enriches the
/// database with information that can be used to work with the schema, without
Expand Down Expand Up @@ -88,16 +88,7 @@ impl ParserDatabase {

/// See the docs on [ParserDatabase](/struct.ParserDatabase.html).
pub fn new(schemas: Vec<(String, schema_ast::SourceFile)>, diagnostics: &mut Diagnostics) -> Self {
let asts = schemas
.into_iter()
.enumerate()
.map(|(file_idx, (path, source))| {
let id = FileId(file_idx as u32);
let ast = schema_ast::parse_schema(source.as_str(), diagnostics, id);
(path, source, ast)
})
.collect();
let asts = Files(asts);
let asts = Files::new(schemas, diagnostics);

let mut interner = Default::default();
let mut names = Default::default();
Expand Down Expand Up @@ -161,6 +152,12 @@ impl ParserDatabase {
}
}

/// Render the given diagnostics (warnings + errors) into a String.
/// This method is multi-file aware.
pub fn render_diagnostics(&self, diagnostics: &Diagnostics) -> String {
self.asts.render_diagnostics(diagnostics)
}

/// The parsed AST. This methods asserts that there is a single prisma schema file. As
/// multi-file schemas are implemented, calls to this methods should be replaced with
/// `ParserDatabase::ast()` and `ParserDatabase::iter_asts()`.
Expand Down
6 changes: 6 additions & 0 deletions psl/psl-core/src/configuration/configuration_struct.rs
Expand Up @@ -14,6 +14,12 @@ pub struct Configuration {
}

impl Configuration {
pub fn extend(&mut self, configuration: Configuration) {
self.generators.extend(configuration.generators);
self.datasources.extend(configuration.datasources);
self.warnings.extend(configuration.warnings);
}

pub fn validate_that_one_datasource_is_provided(&self) -> Result<(), Diagnostics> {
if self.datasources.is_empty() {
Err(DatamodelError::new_validation_error(
Expand Down
35 changes: 22 additions & 13 deletions psl/psl-core/src/lib.rs
Expand Up @@ -29,7 +29,7 @@ pub use set_config_dir::set_config_dir;

use self::validate::{datasource_loader, generator_loader};
use diagnostics::Diagnostics;
use parser_database::{ast, ParserDatabase, SourceFile};
use parser_database::{ast, Files, ParserDatabase, SourceFile};

/// The collection of all available connectors.
pub type ConnectorRegistry<'a> = &'a [&'static dyn datamodel_connector::Connector];
Expand All @@ -54,18 +54,7 @@ impl ValidatedSchema {
}

pub fn render_own_diagnostics(&self) -> String {
self.render_diagnostics(&self.diagnostics)
}

pub fn render_diagnostics(&self, diagnostics: &Diagnostics) -> String {
let mut out = Vec::new();

for error in diagnostics.errors() {
let (file_name, source, _) = &self.db[error.span().file_id];
error.pretty_print(&mut out, file_name, source.as_str()).unwrap();
}

String::from_utf8(out).unwrap()
self.db.render_diagnostics(&self.diagnostics)
}
}

Expand Down Expand Up @@ -149,6 +138,26 @@ pub fn parse_configuration(
diagnostics.to_result().map(|_| out)
}

pub fn parse_configuration_multi_file(
files: Vec<(String, SourceFile)>,
connectors: ConnectorRegistry<'_>,
) -> Result<(Files, Configuration), (Files, diagnostics::Diagnostics)> {
let mut diagnostics = Diagnostics::default();
let mut configuration = Configuration::default();

let asts = Files::new(files, &mut diagnostics);

for (_, _, _, ast) in asts.iter() {
let out = validate_configuration(ast, &mut diagnostics, connectors);
configuration.extend(out);
}
Comment on lines +148 to +153
Copy link
Contributor Author

@jkomyno jkomyno Apr 11, 2024

Choose a reason for hiding this comment

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

I purposedly avoided creating a ParserDatabase instance here, as it performs additional validations that are not retro-compatible with parse_configuration, used in prisma_fmt::get_config.


match diagnostics.to_result() {
Ok(_) => Ok((asts, configuration)),
Err(err) => Err((asts, err)),
}
}

fn validate_configuration(
schema_ast: &ast::SchemaAst,
diagnostics: &mut Diagnostics,
Expand Down
9 changes: 9 additions & 0 deletions psl/psl/src/lib.rs
Expand Up @@ -2,6 +2,7 @@
#![deny(rust_2018_idioms, unsafe_code, missing_docs)]

pub use psl_core::builtin_connectors;
use psl_core::parser_database::Files;
pub use psl_core::{
builtin_connectors::{can_have_capability, can_support_relation_load_strategy, has_capability},
datamodel_connector,
Expand Down Expand Up @@ -40,6 +41,14 @@ pub fn parse_configuration(schema: &str) -> Result<Configuration, Diagnostics> {
psl_core::parse_configuration(schema, builtin_connectors::BUILTIN_CONNECTORS)
}

/// Parses and validates Prisma schemas, but skip analyzing everything except datasource and generator
/// blocks.
pub fn parse_configuration_multi_file(
files: Vec<(String, SourceFile)>,
) -> Result<(Files, Configuration), (Files, Diagnostics)> {
psl_core::parse_configuration_multi_file(files, builtin_connectors::BUILTIN_CONNECTORS)
}

/// Parse and analyze a Prisma schema.
pub fn parse_schema(file: impl Into<SourceFile>) -> Result<ValidatedSchema, String> {
let mut schema = validate(file.into());
Expand Down
2 changes: 0 additions & 2 deletions psl/schema-ast/src/ast/find_at_position.rs
Expand Up @@ -354,9 +354,7 @@ impl<'ast> SourcePosition<'ast> {
pub enum PropertyPosition<'ast> {
/// prop
Property,
///
Value(&'ast str),
///
FunctionValue(&'ast str),
}

Expand Down