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

Fix clang-tidy warnings to make it actually useful #1048

Open
wants to merge 39 commits into
base: main
Choose a base branch
from

Conversation

azrogers
Copy link
Contributor

When we first added clang-tidy to Cesium Native, we left the warnings as merely informational, because there were too many of them to fix at the moment. Unfortunately, this leaves clang-tidy as an afterthought, because I don't imagine many of us are poking through the CI logs to see if our changes produced any new clang-tidy warnings when there's already hundreds of existing warnings. This change attempts to fix this: resolve the existing warnings, enable any useful checks that weren't already enabled, and treat the warnings as errors so we'll notice them.

Changes in this PR:

  • clang-tidy on CI now uses clang-tidy version 19, because earlier versions don't support the ExcludeHeaderFilterRegex config option we're using to remove ezvcpkg headers from the build.
  • We are now following Include-What-You-Use, thanks to clang-tidy's misc-include-cleaner check. This has resulted in pretty much every file in the project receiving a change, but it's worth it.
  • Every bugprone clang-tidy check has been enabled, with the following exceptions:
    • bugprone-exception-escape, as fixing this is already a pending issue (Functions are declared noexcept, even though they may throw #348).
    • bugprone-misplaced-widening-cast, as it results in extremely verbose lines (like size_t pixels = static_cast<size_t>(image.width) * static_cast<size_t>(image.height) * static_cast<size_t>(image.channels) instead of just size_t pixels = static_cast<size_t>(image.width * image.height * image.channels).
    • bugprone-unhandled-exception-at-new, as this is both part of Functions are declared noexcept, even though they may throw #348, and also extremely nitpicky - I don't think we need to wrap every call to new in a try...catch.
  • Most modernize and misc checks have been enabled, where they didn't conflict with the style guide or produce way too many changes for one PR. The following I decided were too much for just this PR, but they can be automatically applied at a later date using clang-tidy-fix:
    • misc-const-correctness detects every variable that could be declared const.
    • modernize-loop-convert uses range-based for loops instead of manually advancing the iterator.
    • modernize-type-traits changes traits<...>::type and traits<..>::value to traits_t<...> and traits_v<...>
    • modernize-use-constraints replaces std::enable_if with C++20 requires clauses.
    • modernize-use-using replaces typedef with using.
    • modernize-return-braced-init-list replaces statements like return glm::dvec2(0.5, 0.5); with return {0.5, 0.5};
    • modernize-use-designated-initializers would mean adding .field= to initializer statements, like {.x = 0.5, .y = 0.5}, which would prevent bugs when changing the order of fields.
  • All performance checks have been enabled, with the exception of performance-enum-size, as I felt that saving three bytes by defining an enum with int8_t instead of the default int32_t didn't seem like it was worth the change.
  • readability checks are mostly style guide-related, but might be worth looking into. readability-else-after-return and readability-braces-around-statements seem like they could be good to add.
  • All warnings are now treated as errors.

Copy link
Member

@kring kring left a comment

Choose a reason for hiding this comment

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

Thanks @azrogers! I've so far reviewed only the Cesium3DTiles, Cesium3DTilesContent, Cesium3DTilesReader, and Cesium3DTilesSelection namespaces. Lots of really good changes here! And a few things for me to nitpick (see below).

- name: Install nasm
uses: ilammy/setup-nasm@v1
- name: Install latest ninja and cmake
uses: lukka/get-cmake@latest
Copy link
Member

Choose a reason for hiding this comment

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

Looks like you've removed the nasm install here.
It's unclear if nasm is actually working even before this change (see #1025), but we do want to continue installing it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The motivation here is to speed up clang-tidy a bit by not installing nasm, since it shouldn't affect anything clang-tidy is checking. I can add it back if we still need it for linting though.

Copy link
Member

Choose a reason for hiding this comment

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

Oh right, of course, this is only for linting. That's fine.

Comment on lines 1 to 5
#include "Cesium3DTiles/Class.h"
#include "Cesium3DTiles/ClassProperty.h"
#include "Cesium3DTiles/MetadataEntity.h"
#include "Cesium3DTiles/Schema.h"
#include "CesiumUtility/JsonValue.h"
Copy link
Member

Choose a reason for hiding this comment

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

These should all use <> instead of "". Looks like this same change needs to be done throughout. Sorry!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wrote a Node script (when all you have is a Node.js runtime...) to conform the includes, so local files (private headers included by cpp files in the same folder) use quotes and everything else uses <>.

Cesium3DTiles/src/MetadataQuery.cpp Outdated Show resolved Hide resolved
Comment on lines +735 to +736
const auto& rapidjsonStr =
it->IsNull() ? noDataValue.value_or("null") : it->GetString();
Copy link
Member

Choose a reason for hiding this comment

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

This change shouldn't be needed. We'd go into the if above instead of this else if noDataValue were std::nullopt. I guess clang-tidy isn't able to see that, eh?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We only check if noDataValue is a nullopt if it is null. If it were a number or something other than a string or null, we would enter this else with a possibly nullopt noDataValue.

Copy link
Member

Choose a reason for hiding this comment

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

We only check if noDataValue is a nullopt if it is null.

That's true, but in the else, we only evaluate *noDataValue if it->isNull() returns true. Otherwise we go to the other side of the ternary operator and evaluate it->GetString() instead.

In the case that it->isNull() is true and also noDataValue.has_value() is false, we would go into the if, so the else doesn't matter.

But even if I'm right, just the fact that we're having this conversation might be a sign the compiler can't be expected to figure this out.

@@ -791,6 +816,7 @@ void updateExtensionWithJsonScalarProperty(

for (int64_t i = 0; i < propertyTable.count; ++i, ++p, ++it) {
if (it->IsNull()) {
CESIUM_ASSERT(noDataValue.has_value());
Copy link
Member

Choose a reason for hiding this comment

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

@j9liu can you comment on this change? I think it's dangerous to assert here, because this might represent "bad data" (which should never assert) rather than "developer errors" (which is the only time we should assert). But it's not obvious what the right behavior should be.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think the intention is to guarantee that *noDataValue is a valid value before dereferencing the optional. And it's been a while since I've looked at this code, so I wasn't 100% sure that that will be the case. But after a lookover I'm fairly confident this won't happen.

TL;DR if noDataValue were nullopt, this would be a developer error, and I don't think it hurts to keep the assert.

Rationale below if you would like it....


First we have to execute findCompatibleTypes to narrow down how the property could be represented. For scalar properties, we try to settle on a reasonable sentinel value (-1 or 0) if it contains null entries. At the end, we verify whether any numeric sentinel values are left. And if there are none, we actually default the type to STRING so that we can use the empty string sentinel.

// If no sentinel value is available, then it's not possible to accurately
// represent the null value of this property. Make it a string property
// instead.
if (compatibleTypes.hasNullValue() && !compatibleTypes.getSentinelValue()) {
compatibleTypes.makeIncompatible();
}

Looking at where noData is set.... the scalar property should have a sentinel value to fall back on. Because otherwise, it would have been turned into a string property. But this is definitely not obvious from a glance.

auto maybeSentinel = compatibleTypes.getSentinelValue();
// Try to set the "noData" value before copying the property (to avoid copying
// nulls).
if (compatibleTypes.hasNullValue() && maybeSentinel) {
JsonValue sentinelValue = *maybeSentinel;
// If -1 is the only available sentinel, modify the masked type to only use
// signed integer types (if possible).
if (sentinelValue.getInt64OrDefault(0) == -1) {
type.isUint8 = false;
type.isUint16 = false;
type.isUint32 = false;
type.isUint64 = false;
}
classProperty.noData = sentinelValue;
}

And in the way we handle JSON string property, we do check whether the sentinel value is present. We don't attempt to write the sentinel value if noDataValue is std::nullopt. I think we just give up and try to write null to the output. 😆

for (int64_t i = 0; i < propertyTable.count; ++i) {
if (it == propertyValue.end()) {
rapidjsonOffsets.emplace_back(rapidjsonStrBuffer.GetLength());
continue;
}
if (!it->IsString() || (it->IsNull() && !noDataValue)) {
// Everything else that is not string will be serialized by json
rapidjson::Writer<rapidjson::StringBuffer> writer(rapidjsonStrBuffer);
it->Accept(writer);
} else {
// Because serialized string json will add double quotations in the
// buffer which is not needed by us, we will manually add the string to
// the buffer
const auto& rapidjsonStr = it->IsNull() ? *noDataValue : it->GetString();
rapidjsonStrBuffer.Reserve(it->GetStringLength());
for (rapidjson::SizeType j = 0; j < it->GetStringLength(); ++j) {
rapidjsonStrBuffer.PutUnsafe(rapidjsonStr[j]);
}
}

Cesium3DTilesSelection/src/TilesetHeightQuery.cpp Outdated Show resolved Hide resolved
Cesium3DTilesSelection/src/TilesetJsonLoader.cpp Outdated Show resolved Hide resolved
Cesium3DTilesSelection/src/TilesetJsonLoader.cpp Outdated Show resolved Hide resolved
Cesium3DTilesSelection/src/ViewState.cpp Outdated Show resolved Hide resolved
@azrogers
Copy link
Contributor Author

Disabled CheckTriviallyCopyableMove and re-added what std::moves I could, but some of them are just passed to the functions they're in via const references, so attempting to move them does nothing and clang-tidy complains about this.

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