Skip to content

Migrate ValidationCode to PSI #186

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

Merged
merged 38 commits into from
Jan 24, 2025
Merged

Migrate ValidationCode to PSI #186

merged 38 commits into from
Jan 24, 2025

Conversation

yevhenii-nadtochii
Copy link
Contributor

@yevhenii-nadtochii yevhenii-nadtochii commented Jan 22, 2025

This change set migrates the main Java validation renderer to PSI.

All Java class modifications are now performed within a single method in MessageValidationCode.render(psiFile), allowing for quick grasp of what is actually mutated and how.

This class deals with JavaPoet only in a degree to remain compatible with rules-based CodeGenerator output. These generators produce CodeBlock, MethodSpec and FieldSpec, which are easily converted to PsiStatements (our custom wrapper for PsiCodeBlock, see below), PsiMethod and PsiField.

Note: this PR doesn't bring new features, only migration to PSI and refactoring. Another preparational step to introduce option-specific generators.

Particular changes

  1. Explicitly mentioned in docs to @Validated and @NonValidated that these annotations are not meant for runtime.
  2. Added tests ensuring the generated messages implement ValidatableMessage and ValidatingBuilder interfaces.
  3. CodeGenerator.supportingMemebers() has been split into supportingFields() and supportingMethods(). We can't create general-purpose elements with PSI, only specific ones. Usage of Queriyng were replaced with TypeSystem.
  4. I've made ValidationPlugin abstract, with renderers being expected on the constructor. This makes the declaration of JavaValidationPlugin more straightforward.
  5. MessageValidationCode now contains both modifications of the message class and its builder. I prefer having them both in the same method because they are intertwined. Modifying of Builder.build() doesn't make sense without presence of Message.validate().
  6. Now, we generate only return java.util.Optional.empty(); in validate() when the message doesn't declare constraints. No need to put there an if block, which always evaluates to Optional.empty().
  7. Renamed SetOnceValidationRenderer to SetOnceRenderer. Now, it looks like this Validation word is redundant.

Removed classes

These classes were merged into a single MessageValidationCode:

  1. ImplementValidatingBuilder.
  2. ValidateMethodCode.
  3. ValidationCode.
  4. ValidationConstraintsCode.

These classes are no longer needed (all from java.point package):

  1. BuilderInsertionPoint
  2. BuilderMethodReturnTypeAnnotation
  3. BuildPartialReturnTypeAnnotation.
  4. CachingJavaClassSource.
  5. ExtraValidation.
  6. ParsedSources.
  7. ValidateBeforeReturn.
  8. BuilderMethodsSpec was testing the insertion points, not the generated code.
  9. MockPrinter was a fixture for BuilderMethodsSpec.

API suggestions for PSI

  1. PsiClass.nested(simpleName: String): PsiClass – allows easily get a nested Message.Builder when you already have a PsiClass of the Message at your disposal.
  2. PsiElement.findFirstByText(...), which accepts String. I have taken it from (set_once) implementation because this is the most reliable way to find a place to inject the code somewhere in the method body. Even if we know, that we want to inject before the last statement – it doesn't help. The last PsiElement in PsiMethod.body is always right brace. Ok, let's try its previous sibling (should it be return?) – no, it can be return ...;, whitespace or even line break. Depends on the formatting. PSI stores formatting elements along with other elements.
  3. PsiStatements wrapper to avoid dealing with PsiCodeBlock when we need to represent a list of statements. In validation codebase, we treat code blocks as List<Statement>. But within PSI, a code block is more about Java grammar, where it is a list of statements and the curly braces { <statements> }. This sophisticates 1) creating a set of statements from text and 2) inserting them to other code blocks.
  4. PsiClass.findMethodBySignature() and PsiClass.methodWithSignature(), which were introduced for (set_once).

@yevhenii-nadtochii yevhenii-nadtochii self-assigned this Jan 22, 2025
Copy link

codecov bot commented Jan 23, 2025

Codecov Report

Attention: Patch coverage is 1.38889% with 142 lines in your changes missing coverage. Please review.

Project coverage is 32.47%. Comparing base (9748a15) to head (20a0d61).
Report is 39 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff              @@
##             master     #186      +/-   ##
============================================
+ Coverage     32.16%   32.47%   +0.31%     
+ Complexity      329      312      -17     
============================================
  Files           141      131      -10     
  Lines          2786     2590     -196     
  Branches        227      213      -14     
============================================
- Hits            896      841      -55     
+ Misses         1824     1689     -135     
+ Partials         66       60       -6     

@yevhenii-nadtochii yevhenii-nadtochii marked this pull request as ready for review January 24, 2025 14:22
@yevhenii-nadtochii
Copy link
Contributor Author

@armiol @alexander-yevsyukov PTAL

Copy link
Collaborator

@alexander-yevsyukov alexander-yevsyukov left a comment

Choose a reason for hiding this comment

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

Please see my comments.

* message which is going to be validated later.
*
* <p>Note: this annotation is not intended to be retained at runtime.
Copy link
Collaborator

Choose a reason for hiding this comment

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

This block repeats the same idea: the retention is CLASS because we don't need RUNTIME. Please rephrase.

@@ -42,6 +42,11 @@
* is overridden with a version which returns only valid messages, that version should be marked
* with this annotation.
*
* <p>Note: this annotation is not intended to be retained at runtime.
Copy link
Collaborator

Choose a reason for hiding this comment

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

This block repeats the same idea: the retention is CLASS because we don't need RUNTIME. Please rephrase.

/**
* Exposes [typeSystem] property, so that the code generation context could use it.
*/
public override val typeSystem: TypeSystem
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's keep the encapsulation here, as discussed vocally.

*
* @param base The base plugin to extend.
* @constructor Creates an instance that contains all the components from the given [base] plugin.
* Note: [`(set_once)`][SetOnceRenderer] option is rendered with its own
Copy link
Collaborator

Choose a reason for hiding this comment

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

It's not the option which is rendered. We render validation code serving the (set_once) constraint.

I would start the explanation of the matter with the enumeration of the renderers included into the ValidationPlugin, and then explain them both briefly.

|public java.util.Optional<io.spine.validate.ValidationError> validate() {
| ${validateMethodBody(formattedConstraints)}
|}
""".trimMargin(), this
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why do we use the style which requires trimMargin() here, and the trimIndent() style below?

Copy link
Collaborator

@armiol armiol left a comment

Choose a reason for hiding this comment

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

@yevhenii-nadtochii LGTM in general. Please see my comments.

* Note: [`(set_once)`][SetOnceRenderer] validation code is rendered with its own
* renderer because it significantly differs from the rest of constraints. This option
* modifies the message builder behavior. The rest of the constraints are more about
* message state assertions.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Did you mean "builder state assertions"?

* implement [io.spine.validate.ValidatingBuilder] interface. Logically, it is a part
* of this renderer, but as for now, it is a standalone renderer before this class
* migrates to PSI.
* This rendered is applied to every [Message] being compiled, even if the message
Copy link
Collaborator

Choose a reason for hiding this comment

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

"This rendered ..." has some grammar issues.

@@ -0,0 +1,237 @@
/*
* Copyright 2024, TeamDev. All rights reserved.
Copy link
Collaborator

Choose a reason for hiding this comment

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

It's 2025.

* All validation logic is injected into the [ValidatableMessage.validate] method
* within the message class itself. This allows a message instance to be validated even
* after it has been built. Note that this method does not throw exceptions directly;
* instead, it returns any detected violations, if any.
Copy link
Collaborator

Choose a reason for hiding this comment

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

"...it returns the detected violations, if any."

* instead, it returns any detected violations, if any.
*
* The message builder is modified to invoke the [ValidatableMessage.validate] just
* before returning from its [build][com.google.protobuf.Message.Builder.build] method.
Copy link
Collaborator

Choose a reason for hiding this comment

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

"before returning the result from ..."

}

/**
* Generates a Java [constraint][constraints], [supportingFields] and [supportingMethods]
Copy link
Collaborator

Choose a reason for hiding this comment

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

"a" cannot be used with plurals. Please review.

* 1. [constraints] are joined to [String] using [joinToString] instead of `joinByLine()`
* because the contained code blocks already have new lines when converted to string.
* 2. For the same reason, we trim the result because a trailing empty line is not needed.
* 3. When creating PSI method from the text, we have to use [trimMargin] and '|' symbols
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please review this p.3 using any grammar checker. I personally use Grazie (as a Chrome extension).

I suspect some prepositions are missing.

* Searches for a method in this [PsiClass] matching the given signature
* Returns a nested class declared in this [PsiClass].
*
* @param simpleName The class simple name.
Copy link
Collaborator

Choose a reason for hiding this comment

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

"The simple name of the class."

* The basic implementation of ProtoData validation plugin, which builds
* language-agnostic representation of the declared constraints.
*
* The concrete implementations should provide [renderers], which will
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would remove "will".

@yevhenii-nadtochii
Copy link
Contributor Author

@armiol @alexander-yevsyukov PTAL

Copy link
Collaborator

@alexander-yevsyukov alexander-yevsyukov left a comment

Choose a reason for hiding this comment

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

LGTM

@yevhenii-nadtochii yevhenii-nadtochii merged commit d0f1c9b into master Jan 24, 2025
7 checks passed
@yevhenii-nadtochii yevhenii-nadtochii deleted the migrate-to-PSI branch January 24, 2025 17:46
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