Skip to content

Commit 41b938d

Browse files
authored
Squash assorted Clippy and Rust doc warnings in codegen integration tests (#3684)
This commit gets rid of some Clippy and Rust doc warnings we produce in generated code, which are surfaced in our codegen integration tests. The aim is that eventually we will be able to deny Clippy and Rust doc warnings in #3678 (despite us baking in `RUSTDOCFLAGS="-D warnings"` and `RUSTFLAGS="-D warnings"` in our CI Docker image, we curently clear these in codegen integration tests). Note that denying compiler warnings is separately tracked in #3194. The commit contains fixes for the following: - Unconditionally referring to the `crate::types` module in Rust docs when the Smithy model is such that it is not generated. - Broken intra-crate doc links for client documentation. - Incorrectly escaping Rust reserved keywords when referring to operation names in Rust docs. - An unnecessary semi-colon when rendering additional client plugins. - Not escaping brackets in text when rendering Rust docs containing HTML, that are interpreted as broken links. - A broken link to unit variants of unions. - Using `TryFrom` instead of `From` when infallibly converting from `&str` to `String` when deserializing an `@httpPayload` in the server. - Using a redundant closure with `unwrap_or_else` when falling back to a `@default` boolean or number value for a `document` shape, instead of `unwrap_or`. - Not opting into `clippy::enum_variant_names` when rendering the `Operation` enum in the server (since we render the operation names that were modeled as-is). ---- _By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice._
1 parent b7f12a6 commit 41b938d

File tree

12 files changed

+145
-41
lines changed

12 files changed

+145
-41
lines changed

codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/ClientRustModule.kt

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@ package software.amazon.smithy.rust.codegen.client.smithy
77

88
import software.amazon.smithy.codegen.core.Symbol
99
import software.amazon.smithy.model.Model
10+
import software.amazon.smithy.model.shapes.EnumShape
1011
import software.amazon.smithy.model.shapes.OperationShape
1112
import software.amazon.smithy.model.shapes.Shape
13+
import software.amazon.smithy.model.shapes.StringShape
1214
import software.amazon.smithy.model.shapes.StructureShape
1315
import software.amazon.smithy.model.shapes.UnionShape
16+
import software.amazon.smithy.model.traits.EnumTrait
1417
import software.amazon.smithy.model.traits.ErrorTrait
1518
import software.amazon.smithy.rust.codegen.client.smithy.generators.client.FluentClientDocs
1619
import software.amazon.smithy.rust.codegen.client.smithy.generators.client.FluentClientGenerator
@@ -195,8 +198,9 @@ object ClientModuleProvider : ModuleProvider {
195198
override fun moduleForShape(
196199
context: ModuleProviderContext,
197200
shape: Shape,
198-
): RustModule.LeafModule =
199-
when (shape) {
201+
): RustModule.LeafModule {
202+
fun shouldNotBeRendered(): Nothing = PANIC("Shape ${shape.id} should not be rendered in any module")
203+
return when (shape) {
200204
is OperationShape -> perOperationModule(context, shape)
201205
is StructureShape ->
202206
when {
@@ -206,8 +210,18 @@ object ClientModuleProvider : ModuleProvider {
206210
else -> ClientRustModule.types
207211
}
208212

209-
else -> ClientRustModule.types
213+
is UnionShape, is EnumShape -> ClientRustModule.types
214+
is StringShape -> {
215+
if (shape.hasTrait<EnumTrait>()) {
216+
ClientRustModule.types
217+
} else {
218+
shouldNotBeRendered()
219+
}
220+
}
221+
222+
else -> shouldNotBeRendered()
210223
}
224+
}
211225

212226
override fun moduleForOperationError(
213227
context: ModuleProviderContext,

codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/ClientDocsGenerator.kt

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ package software.amazon.smithy.rust.codegen.client.smithy.customizations
77

88
import software.amazon.smithy.model.traits.TitleTrait
99
import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext
10+
import software.amazon.smithy.rust.codegen.client.smithy.ClientRustModule
1011
import software.amazon.smithy.rust.codegen.core.rustlang.Writable
1112
import software.amazon.smithy.rust.codegen.core.rustlang.containerDocs
1213
import software.amazon.smithy.rust.codegen.core.rustlang.writable
14+
import software.amazon.smithy.rust.codegen.core.smithy.DirectedWalker
1315
import software.amazon.smithy.rust.codegen.core.smithy.generators.LibRsCustomization
1416
import software.amazon.smithy.rust.codegen.core.smithy.generators.LibRsSection
1517
import software.amazon.smithy.rust.codegen.core.smithy.generators.ModuleDocSection
@@ -30,6 +32,22 @@ class ClientDocsGenerator(private val codegenContext: ClientCodegenContext) : Li
3032

3133
private fun crateLayout(): Writable =
3234
writable {
35+
val hasTypesModule =
36+
DirectedWalker(codegenContext.model).walkShapes(codegenContext.serviceShape)
37+
.any {
38+
try {
39+
codegenContext.symbolProvider.moduleForShape(it).name == ClientRustModule.types.name
40+
} catch (ex: RuntimeException) {
41+
// The shape should not be rendered in any module.
42+
false
43+
}
44+
}
45+
val typesModuleSentence =
46+
if (hasTypesModule) {
47+
"These structs and enums live in [`types`](crate::types). "
48+
} else {
49+
""
50+
}
3351
val serviceName = codegenContext.serviceShape.getTrait<TitleTrait>()?.value ?: "the service"
3452
containerDocs(
3553
"""
@@ -40,7 +58,7 @@ class ClientDocsGenerator(private val codegenContext: ClientCodegenContext) : Li
4058
either a successful output or a [`SdkError`](crate::error::SdkError).
4159
4260
Some of these API inputs may be structs or enums to provide more complex structured information.
43-
These structs and enums live in [`types`](crate::types). There are some simpler types for
61+
${typesModuleSentence}There are some simpler types for
4462
representing data such as date times or binary blobs that live in [`primitives`](crate::primitives).
4563
4664
All types required to configure a client via the [`Config`](crate::Config) struct live

codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientDocs.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ object FluentClientDocs {
3434
or identity resolver to be configured. The config is used to customize various aspects of the client,
3535
such as:
3636
37-
- [HTTP Connector](crate::config::Builder::http_connector)
38-
- [Retry](crate::config::Builder::retry_config)
37+
- [The underlying HTTP client](crate::config::Builder::http_client)
38+
- [Retries](crate::config::Builder::retry_config)
3939
- [Timeouts](crate::config::Builder::timeout_config)
4040
- [... and more](crate::config::Builder)
4141
@@ -76,7 +76,7 @@ object FluentClientDocs {
7676
if (operation != null && member != null) {
7777
val operationSymbol = symbolProvider.toSymbol(operation)
7878
val memberSymbol = symbolProvider.toSymbol(member)
79-
val operationFnName = FluentClientGenerator.clientOperationFnName(operation, symbolProvider)
79+
val operationFnName = FluentClientGenerator.clientOperationFnDocsName(operation, symbolProvider)
8080
docsTemplate(
8181
"""
8282
## Using the `Client`

codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientGenerator.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,13 @@ class FluentClientGenerator(
5959
fun clientOperationFnName(
6060
operationShape: OperationShape,
6161
symbolProvider: RustSymbolProvider,
62-
): String = RustReservedWords.escapeIfNeeded(symbolProvider.toSymbol(operationShape).name.toSnakeCase())
62+
): String = RustReservedWords.escapeIfNeeded(clientOperationFnDocsName(operationShape, symbolProvider))
63+
64+
/** When using the function name in Rust docs, there's no need to escape Rust reserved words. **/
65+
fun clientOperationFnDocsName(
66+
operationShape: OperationShape,
67+
symbolProvider: RustSymbolProvider,
68+
): String = symbolProvider.toSymbol(operationShape).name.toSnakeCase()
6369

6470
fun clientOperationModuleName(
6571
operationShape: OperationShape,
@@ -277,7 +283,7 @@ private fun baseClientRuntimePluginsFn(
277283
.with_client_plugin(crate::config::ServiceRuntimePlugin::new(config.clone()))
278284
.with_client_plugin(#{NoAuthRuntimePlugin}::new());
279285
280-
#{additional_client_plugins:W};
286+
#{additional_client_plugins:W}
281287
282288
for plugin in configured_plugins {
283289
plugins = plugins.with_client_plugin(plugin);

codegen-core/common-test-models/misc.smithy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use smithy.framework#ValidationException
99

1010
/// A service to test miscellaneous aspects of code generation where protocol
1111
/// selection is not relevant. If you want to test something protocol-specific,
12-
/// add it to a separate `<protocol>-extras.smithy`.
12+
/// add it to a separate `[protocol]-extras.smithy`.
1313
@restJson1
1414
@title("MiscService")
1515
service MiscService {

codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustWriter.kt

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package software.amazon.smithy.rust.codegen.core.rustlang
88
import org.intellij.lang.annotations.Language
99
import org.jsoup.Jsoup
1010
import org.jsoup.nodes.Element
11+
import org.jsoup.select.Elements
1112
import software.amazon.smithy.codegen.core.CodegenException
1213
import software.amazon.smithy.codegen.core.Symbol
1314
import software.amazon.smithy.codegen.core.SymbolDependencyContainer
@@ -336,10 +337,10 @@ fun <T : AbstractCodeWriter<T>> T.docsOrFallback(
336337
docs("_Note: ${it}_")
337338
}
338339
} else if (autoSuppressMissingDocs) {
340+
// Otherwise, suppress the missing docs lint for this shape since
341+
// the lack of documentation is a modeling issue rather than a codegen issue.
339342
rust("##[allow(missing_docs)] // documentation missing in model")
340343
}
341-
// Otherwise, suppress the missing docs lint for this shape since
342-
// the lack of documentation is a modeling issue rather than a codegen issue.
343344
return this
344345
}
345346

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

442-
/** Parse input as HTML and normalize it */
443+
/** Parse input as HTML and normalize it, for suitable use within Rust docs. */
443444
fun normalizeHtml(input: String): String {
444445
val doc = Jsoup.parse(input)
445446
doc.body().apply {
446-
normalizeAnchors() // Convert anchor tags missing href attribute into pre tags
447+
normalizeAnchors()
448+
escapeBracketsInText()
447449
}
448450

449451
return doc.body().html()
450452
}
451453

454+
/**
455+
* Escape brackets in elements' inner text.
456+
*
457+
* For example:
458+
*
459+
* ```html
460+
* <body>
461+
* <p>Text without brackets</p>
462+
* <div>Some text with [brackets]</div>
463+
* <span>Another [example, 1]</span>
464+
* </body>
465+
* ```
466+
*
467+
* gets converted to:
468+
*
469+
* ```html
470+
* <body>
471+
* <p>Text without brackets</p>
472+
* <div>Some text with \[brackets\]</div>
473+
* <span>Another \[example, 1\]</span>
474+
* </body>
475+
* ```
476+
*
477+
* This is useful when rendering Rust docs, since text within brackets might get misinterpreted as broken Markdown doc
478+
* links (`[link text](https://example.com)`).
479+
**/
480+
private fun Element.escapeBracketsInText() {
481+
// Select all elements that directly contain text with opening or closing brackets.
482+
val elements: Elements = select("*:containsOwn([), *:containsOwn(])")
483+
484+
// Loop through each element and escape the brackets in the text.
485+
for (element: Element in elements) {
486+
val originalText = element.text()
487+
val escapedText = originalText.replace("[", "\\[").replace("]", "\\]")
488+
element.text(escapedText)
489+
}
490+
}
491+
492+
/** Convert anchor tags missing `href` attribute into `code` tags. **/
452493
private fun Element.normalizeAnchors() {
453494
getElementsByTag("a").forEach {
454495
val link = it.attr("href")

codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/UnionGenerator.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,8 @@ private fun RustWriter.renderAsVariant(
200200
) {
201201
if (member.isTargetUnit()) {
202202
rust(
203-
"/// Tries to convert the enum instance into [`$variantName`], extracting the inner `()`.",
203+
"/// Tries to convert the enum instance into [`$variantName`](#T::$variantName), extracting the inner `()`.",
204+
unionSymbol,
204205
)
205206
rust("/// Returns `Err(&Self)` if it can't be converted.")
206207
rustBlockTemplate("pub fn as_$funcNamePart(&self) -> #{Result}<(), &Self>", *preludeScope) {

codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/http/HttpBindingGenerator.kt

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -332,17 +332,13 @@ class HttpBindingGenerator(
332332
}
333333
}
334334
if (targetShape.hasTrait<EnumTrait>()) {
335-
if (codegenTarget == CodegenTarget.SERVER) {
336-
rust(
337-
"Ok(#T::try_from(body_str)?)",
338-
symbolProvider.toSymbol(targetShape),
339-
)
340-
} else {
341-
rust(
342-
"Ok(#T::from(body_str))",
343-
symbolProvider.toSymbol(targetShape),
344-
)
345-
}
335+
// - In servers, `T` is an unconstrained `String` that will be constrained when building the
336+
// builder.
337+
// - In clients, `T` will directly be the target generated enum type.
338+
rust(
339+
"Ok(#T::from(body_str))",
340+
symbolProvider.toSymbol(targetShape),
341+
)
346342
} else {
347343
rust("Ok(body_str.to_string())")
348344
}

codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustWriterTest.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,21 @@ class RustWriterTest {
9999
output shouldContain "/// Top level module"
100100
}
101101

102+
@Test
103+
fun `normalize HTML`() {
104+
val output =
105+
normalizeHtml(
106+
"""
107+
<a>Link without href attribute</a>
108+
<div>Some text with [brackets]</div>
109+
<span>] mismatched [ is escaped too</span>
110+
""",
111+
)
112+
output shouldContain "<code>Link without href attribute</code>"
113+
output shouldContain "Some text with \\[brackets\\]"
114+
output shouldContain "\\] mismatched \\[ is escaped too"
115+
}
116+
102117
@Test
103118
fun `generate doc links`() {
104119
val model =

codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGeneratorCommon.kt

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ import software.amazon.smithy.rust.codegen.core.rustlang.rust
3838
import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
3939
import software.amazon.smithy.rust.codegen.core.rustlang.writable
4040
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig
41+
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
42+
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType.Companion.preludeScope
4143
import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProvider
4244
import software.amazon.smithy.rust.codegen.core.smithy.generators.PrimitiveInstantiator
4345
import software.amazon.smithy.rust.codegen.core.smithy.rustType
@@ -108,15 +110,17 @@ fun generateFallbackCodeToDefaultValue(
108110
"DefaultValue" to defaultValue,
109111
)
110112
} else {
111-
when (targetShape) {
112-
is NumberShape, is EnumShape, is BooleanShape -> {
113-
writer.rustTemplate(".unwrap_or(#{DefaultValue:W})", "DefaultValue" to defaultValue)
114-
}
115-
// Values for the Rust types of the rest of the shapes require heap allocations, so we calculate them
116-
// in a (lazily-executed) closure for slight performance gains.
117-
else -> {
118-
writer.rustTemplate(".unwrap_or_else(|| #{DefaultValue:W})", "DefaultValue" to defaultValue)
119-
}
113+
val node = member.expectTrait<DefaultTrait>().toNode()!!
114+
if ((targetShape is DocumentShape && (node is BooleanNode || node is NumberNode)) ||
115+
targetShape is BooleanShape ||
116+
targetShape is NumberShape ||
117+
targetShape is EnumShape
118+
) {
119+
writer.rustTemplate(".unwrap_or(#{DefaultValue:W})", "DefaultValue" to defaultValue)
120+
} else {
121+
// Values for the Rust types of the rest of the shapes might require heap allocations,
122+
// so we calculate them in a (lazily-executed) closure for minimal performance gains.
123+
writer.rustTemplate(".unwrap_or_else(##[allow(clippy::redundant_closure)] || #{DefaultValue:W})", "DefaultValue" to defaultValue)
120124
}
121125
}
122126
}
@@ -125,7 +129,7 @@ fun generateFallbackCodeToDefaultValue(
125129
* Returns a writable to construct a Rust value of the correct type holding the modeled `@default` value on the
126130
* [member] shape.
127131
*/
128-
fun defaultValue(
132+
private fun defaultValue(
129133
model: Model,
130134
runtimeConfig: RuntimeConfig,
131135
symbolProvider: RustSymbolProvider,
@@ -164,12 +168,12 @@ fun defaultValue(
164168
}
165169
is ListShape -> {
166170
check(node is ArrayNode && node.isEmpty)
167-
rust("Vec::new()")
171+
rustTemplate("#{Vec}::new()", *preludeScope)
168172
}
169173

170174
is MapShape -> {
171175
check(node is ObjectNode && node.isEmpty)
172-
rust("std::collections::HashMap::new()")
176+
rustTemplate("#{HashMap}::new()", "HashMap" to RuntimeType.HashMap)
173177
}
174178

175179
is DocumentShape -> {
@@ -188,7 +192,8 @@ fun defaultValue(
188192

189193
is StringNode ->
190194
rustTemplate(
191-
"#{SmithyTypes}::Document::String(String::from(${node.value.dq()}))",
195+
"#{SmithyTypes}::Document::String(#{String}::from(${node.value.dq()}))",
196+
*preludeScope,
192197
"SmithyTypes" to types,
193198
)
194199

@@ -207,14 +212,19 @@ fun defaultValue(
207212

208213
is ArrayNode -> {
209214
check(node.isEmpty)
210-
rustTemplate("""#{SmithyTypes}::Document::Array(Vec::new())""", "SmithyTypes" to types)
215+
rustTemplate(
216+
"""#{SmithyTypes}::Document::Array(#{Vec}::new())""",
217+
*preludeScope,
218+
"SmithyTypes" to types,
219+
)
211220
}
212221

213222
is ObjectNode -> {
214223
check(node.isEmpty)
215224
rustTemplate(
216-
"#{SmithyTypes}::Document::Object(std::collections::HashMap::new())",
225+
"#{SmithyTypes}::Document::Object(#{HashMap}::new())",
217226
"SmithyTypes" to types,
227+
"HashMap" to RuntimeType.HashMap,
218228
)
219229
}
220230

0 commit comments

Comments
 (0)