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

Squash assorted Clippy and Rust doc warnings in codegen integration tests #3684

Merged
merged 1 commit into from
Jun 11, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ package software.amazon.smithy.rust.codegen.client.smithy

import software.amazon.smithy.codegen.core.Symbol
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.shapes.EnumShape
import software.amazon.smithy.model.shapes.OperationShape
import software.amazon.smithy.model.shapes.Shape
import software.amazon.smithy.model.shapes.StringShape
import software.amazon.smithy.model.shapes.StructureShape
import software.amazon.smithy.model.shapes.UnionShape
import software.amazon.smithy.model.traits.EnumTrait
import software.amazon.smithy.model.traits.ErrorTrait
import software.amazon.smithy.rust.codegen.client.smithy.generators.client.FluentClientDocs
import software.amazon.smithy.rust.codegen.client.smithy.generators.client.FluentClientGenerator
Expand Down Expand Up @@ -195,8 +198,9 @@ object ClientModuleProvider : ModuleProvider {
override fun moduleForShape(
context: ModuleProviderContext,
shape: Shape,
): RustModule.LeafModule =
when (shape) {
): RustModule.LeafModule {
fun shouldNotBeRendered(): Nothing = PANIC("Shape ${shape.id} should not be rendered in any module")
return when (shape) {
is OperationShape -> perOperationModule(context, shape)
is StructureShape ->
when {
Expand All @@ -206,8 +210,18 @@ object ClientModuleProvider : ModuleProvider {
else -> ClientRustModule.types
}

else -> ClientRustModule.types
is UnionShape, is EnumShape -> ClientRustModule.types
is StringShape -> {
if (shape.hasTrait<EnumTrait>()) {
ClientRustModule.types
} else {
shouldNotBeRendered()
}
}

else -> shouldNotBeRendered()
}
}

override fun moduleForOperationError(
context: ModuleProviderContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ package software.amazon.smithy.rust.codegen.client.smithy.customizations

import software.amazon.smithy.model.traits.TitleTrait
import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext
import software.amazon.smithy.rust.codegen.client.smithy.ClientRustModule
import software.amazon.smithy.rust.codegen.core.rustlang.Writable
import software.amazon.smithy.rust.codegen.core.rustlang.containerDocs
import software.amazon.smithy.rust.codegen.core.rustlang.writable
import software.amazon.smithy.rust.codegen.core.smithy.DirectedWalker
import software.amazon.smithy.rust.codegen.core.smithy.generators.LibRsCustomization
import software.amazon.smithy.rust.codegen.core.smithy.generators.LibRsSection
import software.amazon.smithy.rust.codegen.core.smithy.generators.ModuleDocSection
Expand All @@ -30,6 +32,22 @@ class ClientDocsGenerator(private val codegenContext: ClientCodegenContext) : Li

private fun crateLayout(): Writable =
writable {
val hasTypesModule =
DirectedWalker(codegenContext.model).walkShapes(codegenContext.serviceShape)
.any {
try {
codegenContext.symbolProvider.moduleForShape(it).name == ClientRustModule.types.name
} catch (ex: RuntimeException) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: I would prefer this to be a software.amazon.smithy.codegen.core.CodegenException instead of a RuntimeException, as the former explicitly indicates an exceptional code generation path that we are aware of. Using a RuntimeException might give the impression that there is an issue with the overall runtime or Kotlin libraries being used for code generation.

// The shape should not be rendered in any module.
false
}
}
val typesModuleSentence =
if (hasTypesModule) {
"These structs and enums live in [`types`](crate::types). "
} else {
""
}
val serviceName = codegenContext.serviceShape.getTrait<TitleTrait>()?.value ?: "the service"
containerDocs(
"""
Expand All @@ -40,7 +58,7 @@ class ClientDocsGenerator(private val codegenContext: ClientCodegenContext) : Li
either a successful output or a [`SdkError`](crate::error::SdkError).

Some of these API inputs may be structs or enums to provide more complex structured information.
These structs and enums live in [`types`](crate::types). There are some simpler types for
${typesModuleSentence}There are some simpler types for
Copy link
Contributor

Choose a reason for hiding this comment

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

By the time we reach this code during code generation, haven't we already generated all the necessary Rust code? I'm considering if there's a simpler way to determine whether crate::types was used or not, perhaps by maintaining a flag rather than traversing the model again.

representing data such as date times or binary blobs that live in [`primitives`](crate::primitives).

All types required to configure a client via the [`Config`](crate::Config) struct live
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ object FluentClientDocs {
or identity resolver to be configured. The config is used to customize various aspects of the client,
such as:

- [HTTP Connector](crate::config::Builder::http_connector)
- [Retry](crate::config::Builder::retry_config)
- [The underlying HTTP client](crate::config::Builder::http_client)
- [Retries](crate::config::Builder::retry_config)
- [Timeouts](crate::config::Builder::timeout_config)
- [... and more](crate::config::Builder)

Expand Down Expand Up @@ -76,7 +76,7 @@ object FluentClientDocs {
if (operation != null && member != null) {
val operationSymbol = symbolProvider.toSymbol(operation)
val memberSymbol = symbolProvider.toSymbol(member)
val operationFnName = FluentClientGenerator.clientOperationFnName(operation, symbolProvider)
val operationFnName = FluentClientGenerator.clientOperationFnDocsName(operation, symbolProvider)
docsTemplate(
"""
## Using the `Client`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,13 @@ class FluentClientGenerator(
fun clientOperationFnName(
operationShape: OperationShape,
symbolProvider: RustSymbolProvider,
): String = RustReservedWords.escapeIfNeeded(symbolProvider.toSymbol(operationShape).name.toSnakeCase())
): String = RustReservedWords.escapeIfNeeded(clientOperationFnDocsName(operationShape, symbolProvider))

/** When using the function name in Rust docs, there's no need to escape Rust reserved words. **/
fun clientOperationFnDocsName(
operationShape: OperationShape,
symbolProvider: RustSymbolProvider,
): String = symbolProvider.toSymbol(operationShape).name.toSnakeCase()
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there no standard utility function available for retrieving the Rust type generated for a given Smithy Shape, or do we need to call .name.toSnakeCase() on the Symbol wherever needed?


fun clientOperationModuleName(
operationShape: OperationShape,
Expand Down Expand Up @@ -277,7 +283,7 @@ private fun baseClientRuntimePluginsFn(
.with_client_plugin(crate::config::ServiceRuntimePlugin::new(config.clone()))
.with_client_plugin(#{NoAuthRuntimePlugin}::new());

#{additional_client_plugins:W};
#{additional_client_plugins:W}

for plugin in configured_plugins {
plugins = plugins.with_client_plugin(plugin);
Expand Down
2 changes: 1 addition & 1 deletion codegen-core/common-test-models/misc.smithy
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use smithy.framework#ValidationException

/// A service to test miscellaneous aspects of code generation where protocol
/// selection is not relevant. If you want to test something protocol-specific,
/// add it to a separate `<protocol>-extras.smithy`.
/// add it to a separate `[protocol]-extras.smithy`.
@restJson1
@title("MiscService")
service MiscService {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package software.amazon.smithy.rust.codegen.core.rustlang
import org.intellij.lang.annotations.Language
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
import org.jsoup.select.Elements
import software.amazon.smithy.codegen.core.CodegenException
import software.amazon.smithy.codegen.core.Symbol
import software.amazon.smithy.codegen.core.SymbolDependencyContainer
Expand Down Expand Up @@ -336,10 +337,10 @@ fun <T : AbstractCodeWriter<T>> T.docsOrFallback(
docs("_Note: ${it}_")
}
} else if (autoSuppressMissingDocs) {
// Otherwise, suppress the missing docs lint for this shape since
// the lack of documentation is a modeling issue rather than a codegen issue.
rust("##[allow(missing_docs)] // documentation missing in model")
}
// Otherwise, suppress the missing docs lint for this shape since
// the lack of documentation is a modeling issue rather than a codegen issue.
return this
}

Expand Down Expand Up @@ -439,16 +440,56 @@ fun RustWriter.deprecatedShape(shape: Shape): RustWriter {
fun <T : AbstractCodeWriter<T>> T.escape(text: String): String =
text.replace("$expressionStart", "$expressionStart$expressionStart")

/** Parse input as HTML and normalize it */
/** Parse input as HTML and normalize it, for suitable use within Rust docs. */
fun normalizeHtml(input: String): String {
val doc = Jsoup.parse(input)
doc.body().apply {
normalizeAnchors() // Convert anchor tags missing href attribute into pre tags
normalizeAnchors()
escapeBracketsInText()
}

return doc.body().html()
}

/**
* Escape brackets in elements' inner text.
*
* For example:
*
* ```html
* <body>
* <p>Text without brackets</p>
* <div>Some text with [brackets]</div>
* <span>Another [example, 1]</span>
* </body>
* ```
*
* gets converted to:
*
* ```html
* <body>
* <p>Text without brackets</p>
* <div>Some text with \[brackets\]</div>
* <span>Another \[example, 1\]</span>
* </body>
* ```
*
* This is useful when rendering Rust docs, since text within brackets might get misinterpreted as broken Markdown doc
* links (`[link text](https://example.com)`).
**/
private fun Element.escapeBracketsInText() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Just a thought: Should we do this only if the box brackets are not followed by text in parentheses? This would allow Smithy model authors to write linkable text in their documentation. For example, [this is clickable](https://example.com) should not be escaped.

// Select all elements that directly contain text with opening or closing brackets.
val elements: Elements = select("*:containsOwn([), *:containsOwn(])")

// Loop through each element and escape the brackets in the text.
for (element: Element in elements) {
val originalText = element.text()
val escapedText = originalText.replace("[", "\\[").replace("]", "\\]")
element.text(escapedText)
}
}

/** Convert anchor tags missing `href` attribute into `code` tags. **/
private fun Element.normalizeAnchors() {
getElementsByTag("a").forEach {
val link = it.attr("href")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,8 @@ private fun RustWriter.renderAsVariant(
) {
if (member.isTargetUnit()) {
rust(
"/// Tries to convert the enum instance into [`$variantName`], extracting the inner `()`.",
"/// Tries to convert the enum instance into [`$variantName`](#T::$variantName), extracting the inner `()`.",
unionSymbol,
)
rust("/// Returns `Err(&Self)` if it can't be converted.")
rustBlockTemplate("pub fn as_$funcNamePart(&self) -> #{Result}<(), &Self>", *preludeScope) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -332,17 +332,13 @@ class HttpBindingGenerator(
}
}
if (targetShape.hasTrait<EnumTrait>()) {
if (codegenTarget == CodegenTarget.SERVER) {
rust(
"Ok(#T::try_from(body_str)?)",
symbolProvider.toSymbol(targetShape),
)
} else {
rust(
"Ok(#T::from(body_str))",
symbolProvider.toSymbol(targetShape),
)
}
// - In servers, `T` is an unconstrained `String` that will be constrained when building the
// builder.
// - In clients, `T` will directly be the target generated enum type.
rust(
"Ok(#T::from(body_str))",
symbolProvider.toSymbol(targetShape),
)
} else {
rust("Ok(body_str.to_string())")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,21 @@ class RustWriterTest {
output shouldContain "/// Top level module"
}

@Test
fun `normalize HTML`() {
val output =
normalizeHtml(
"""
<a>Link without href attribute</a>
<div>Some text with [brackets]</div>
<span>] mismatched [ is escaped too</span>
""",
)
output shouldContain "<code>Link without href attribute</code>"
output shouldContain "Some text with \\[brackets\\]"
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Should we use """Some text with \[brackets\]""" for increased readability?

output shouldContain "\\] mismatched \\[ is escaped too"
}

@Test
fun `generate doc links`() {
val model =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import software.amazon.smithy.rust.codegen.core.rustlang.rust
import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
import software.amazon.smithy.rust.codegen.core.rustlang.writable
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType.Companion.preludeScope
import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProvider
import software.amazon.smithy.rust.codegen.core.smithy.generators.PrimitiveInstantiator
import software.amazon.smithy.rust.codegen.core.smithy.rustType
Expand Down Expand Up @@ -108,15 +110,17 @@ fun generateFallbackCodeToDefaultValue(
"DefaultValue" to defaultValue,
)
} else {
when (targetShape) {
is NumberShape, is EnumShape, is BooleanShape -> {
writer.rustTemplate(".unwrap_or(#{DefaultValue:W})", "DefaultValue" to defaultValue)
}
// Values for the Rust types of the rest of the shapes require heap allocations, so we calculate them
// in a (lazily-executed) closure for slight performance gains.
else -> {
writer.rustTemplate(".unwrap_or_else(|| #{DefaultValue:W})", "DefaultValue" to defaultValue)
}
val node = member.expectTrait<DefaultTrait>().toNode()!!
if ((targetShape is DocumentShape && (node is BooleanNode || node is NumberNode)) ||
Copy link
Contributor

Choose a reason for hiding this comment

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

Thinking out loud: Would defining a function, such as isBasicRustType(targetShape), provide any advantages, or is this the only place where we check for this?

targetShape is BooleanShape ||
targetShape is NumberShape ||
targetShape is EnumShape
) {
writer.rustTemplate(".unwrap_or(#{DefaultValue:W})", "DefaultValue" to defaultValue)
} else {
// Values for the Rust types of the rest of the shapes might require heap allocations,
// so we calculate them in a (lazily-executed) closure for minimal performance gains.
writer.rustTemplate(".unwrap_or_else(##[allow(clippy::redundant_closure)] || #{DefaultValue:W})", "DefaultValue" to defaultValue)
}
}
}
Expand All @@ -125,7 +129,7 @@ fun generateFallbackCodeToDefaultValue(
* Returns a writable to construct a Rust value of the correct type holding the modeled `@default` value on the
* [member] shape.
*/
fun defaultValue(
private fun defaultValue(
model: Model,
runtimeConfig: RuntimeConfig,
symbolProvider: RustSymbolProvider,
Expand Down Expand Up @@ -164,12 +168,12 @@ fun defaultValue(
}
is ListShape -> {
check(node is ArrayNode && node.isEmpty)
rust("Vec::new()")
rustTemplate("#{Vec}::new()", *preludeScope)
}

is MapShape -> {
check(node is ObjectNode && node.isEmpty)
rust("std::collections::HashMap::new()")
rustTemplate("#{HashMap}::new()", "HashMap" to RuntimeType.HashMap)
}

is DocumentShape -> {
Expand All @@ -188,7 +192,8 @@ fun defaultValue(

is StringNode ->
rustTemplate(
"#{SmithyTypes}::Document::String(String::from(${node.value.dq()}))",
"#{SmithyTypes}::Document::String(#{String}::from(${node.value.dq()}))",
*preludeScope,
"SmithyTypes" to types,
)

Expand All @@ -207,14 +212,19 @@ fun defaultValue(

is ArrayNode -> {
check(node.isEmpty)
rustTemplate("""#{SmithyTypes}::Document::Array(Vec::new())""", "SmithyTypes" to types)
rustTemplate(
"""#{SmithyTypes}::Document::Array(#{Vec}::new())""",
*preludeScope,
"SmithyTypes" to types,
)
}

is ObjectNode -> {
check(node.isEmpty)
rustTemplate(
"#{SmithyTypes}::Document::Object(std::collections::HashMap::new())",
"#{SmithyTypes}::Document::Object(#{HashMap}::new())",
"SmithyTypes" to types,
"HashMap" to RuntimeType.HashMap,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,7 @@ class ServerServiceGenerator(
rustTemplate(
"""
/// An enumeration of all [operations](https://smithy.io/2.0/spec/service-types.html##operation) in $serviceName.
##[allow(clippy::enum_variant_names)]
##[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum Operation {
$operations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ class ServerRequestBindingGenerator(
HttpBindingGenerator(
protocol,
codegenContext,
// Note how we parse the HTTP-bound values into _unconstrained_ types; they will be constrained when
// building the builder.
codegenContext.unconstrainedShapeSymbolProvider,
operationShape,
listOf(
Expand Down