Files
brave-core/docs/best-practices/coding-standards-apis.md
cdesouza-chromium 9a1d93fa73 [docs] Auto format best-practices (#36804)
This PR runs `prettier`'s formatting for all markdown documents under
best practices. This ia completely mechanical change. Additionally, we
also update the claude `add-best-practice` skill to be aware of the
auto-formatter.

Bug: N/A
2026-05-29 17:38:23 +01:00

43 KiB

C++ API Usage, Containers & Type Safety

Use Existing Utilities Instead of Custom Code

Always check for existing well-tested utilities before writing custom code. Chromium and base have extensive libraries for common operations.

// ❌ WRONG - custom query string parsing
std::string ParseQueryParam(const std::string& url, const std::string& key) {
  // custom parsing code...
}

// ✅ CORRECT - use existing utility
net::QueryIterator it(url);
while (!it.IsAtEnd()) {
  if (it.GetKey() == key) return it.GetValue();
  it.Advance();
}

Use base::OnceCallback and base::BindOnce

base::Callback and base::Bind are deprecated. Use base::OnceCallback/base::RepeatingCallback and base::BindOnce/base::BindRepeating. Use std::move when passing or calling a base::OnceCallback.


Never Use std::time - Use base::Time

Always use base::Time and related classes instead of C-style std::time, ctime, or time_t. The base library provides cross-platform, type-safe time utilities.


Use JSONValueConverter for JSON/Type Conversion

When parsing JSON into C++ types, prefer base::JSONValueConverter over manual key-by-key parsing. Manual parsing is verbose, error-prone, and results in duplicated boilerplate.

// ❌ WRONG - manual JSON parsing
const auto* name = dict->FindStringKey("name");
const auto age = dict->FindIntKey("age");
if (name) result.name = *name;
if (age) result.age = *age;

// ✅ CORRECT - use JSONValueConverter
static void RegisterJSONConverter(
    base::JSONValueConverter<MyType>* converter) {
  converter->RegisterStringField("name", &MyType::name);
  converter->RegisterIntField("age", &MyType::age);
}

Use the Right Associative Container

Chromium's container guidelines (base/containers/README.md) recommend specific containers for each use case. std::unordered_map/std::unordered_set are banned.

Default (unordered): Use absl::flat_hash_map and absl::flat_hash_set for general-purpose needs. They provide the best all-around performance for both small and large datasets.

Sorted, write-once or small: Use base::flat_map/base::flat_set. Good cache locality; O(n) mutations are fine for small collections or write-once containers.

Sorted, large, frequently-mutated: Use std::map/std::set. O(log n) mutations matter at scale.

Compile-time lookup tables: Use base::MakeFixedFlatMap/base::MakeFixedFlatSet (see CSA-045).

// ❌ WRONG - banned
std::unordered_map<std::string, double> feature_map_;

// ✅ CORRECT - default unordered container
absl::flat_hash_map<std::string, double> feature_map_;

// ✅ CORRECT - sorted, small/write-once
base::flat_map<std::string, int> lookup_;

// ✅ CORRECT - sorted, large, frequently mutated
std::map<std::string, int> large_mutable_lookup_;

Pointer stability: If you need stable pointers to values, wrap them in std::unique_ptr inside an absl::flat_hash_map. If you need stable pointers to keys, use absl::node_hash_map/absl::node_hash_set (separate node allocation).

// ✅ CORRECT - value pointer stability via unique_ptr wrapping
absl::flat_hash_map<Key, std::unique_ptr<Value>> stable_values;

// ✅ CORRECT - key pointer stability via node hash
absl::node_hash_map<std::string, int> stable_keys;

Also banned: absl::btree_map/absl::btree_set (significant code size penalties in Chromium). See Chromium container guidelines.


Don't Use Deprecated GetAs* Methods on base::Value

The GetAsString(), GetAsInteger(), etc. methods on base::Value are deprecated. Use the newer direct access methods like GetString(), GetInt(), GetDouble().

// ❌ WRONG
std::string str;
value->GetAsString(&str);

// ✅ CORRECT
const std::string& str = value->GetString();

Use GetIfBool/GetIfInt/GetIfString for Safe base::Value Access

When extracting values from a base::Value where the type may not match, use GetIf* accessors instead of Get* which CHECK-fails on type mismatch.

// ❌ WRONG - crashes if value is not a bool
if (value.GetBool()) { ... }

// ✅ CORRECT - safe accessor with value_or
if (value.GetIfBool().value_or(false)) { ... }

Don't Use std::to_string - Use base::NumberToString

std::to_string is on Chromium's deprecated list. Use base::NumberToString instead.

// ❌ WRONG
std::string port_str = std::to_string(port);

// ✅ CORRECT
std::string port_str = base::NumberToString(port);

Don't Use C-Style Casts

Chromium prohibits C-style casts. Use C++ casts (static_cast, reinterpret_cast, etc.) which are safer and more explicit.

// ❌ WRONG
double result = (double)integer_value / total;

// ✅ CORRECT
double result = static_cast<double>(integer_value) / total;

Prefer std::move Over Clone

Use std::move instead of cloning when you don't need the original value anymore. This avoids unnecessary copies. This is especially important when passing std::vector or other large objects to callback .Run() calls — forgetting std::move silently copies the entire buffer.

// ❌ WRONG - copies the entire vector into the callback
std::vector<unsigned char> buffer = BuildData();
std::move(cb).Run(buffer, other_arg);

// ✅ CORRECT - moves the vector, no copy
std::vector<unsigned char> buffer = BuildData();
std::move(cb).Run(std::move(buffer), other_arg);

Don't Create Unnecessary Wrapper Types

Don't create plural/container types when you can use arrays of the singular type. Extra wrapper types add complexity without value.

// ❌ WRONG - unnecessary plural type
struct MonthlyStatements {
  std::vector<MonthlyStatement> statements;
};

// ✅ CORRECT - just use the vector directly
std::vector<MonthlyStatement> GetMonthlyStatements();

Use Pref Dict/List Values Directly

Don't serialize to JSON strings when storing structured data in prefs. Use SetDict/SetList directly instead of JSONWriter::Write + SetString.

// ❌ WRONG - serializing to JSON string unnecessarily
std::string result;
base::JSONWriter::Write(root, &result);
prefs->SetString(prefs::kMyPref, result);

// ✅ CORRECT - use native pref value types
prefs->SetDict(prefs::kMyPref, std::move(dict_value));
prefs->SetList(prefs::kMyPref, std::move(list_value));

Use extern const char[] Over #define for Strings

Use extern const char[] instead of #define for string constants to keep them namespaced.

// ❌ WRONG - pollutes preprocessor namespace
#define MY_URL "https://example.com"

// ✅ CORRECT - properly namespaced
extern const char kMyUrl[];
// In .cc:
const char kMyUrl[] = "https://example.com";

Exception: use #define when you need to pass the value in from GN.


Prefer Enum Types Over String Constants for Typed Values

When a value has a fixed set of valid options, use an enum with string conversion rather than passing raw strings. This enables compiler-checked switch statements and prevents invalid values.

// ❌ WRONG - raw strings
void SetWalletType(const std::string& type);

// ✅ CORRECT - enum with conversion
enum class WalletType { kUphold, kGemini };
void SetWalletType(WalletType type);

No C++ Exceptions in Third-Party Libraries

C++ exceptions are disallowed in Chromium. When integrating third-party libraries, verify they build with exception support disabled.


Use base::EraseIf / std::erase_if Instead of Manual Erase Loops

Prefer base::EraseIf (for base::flat_* containers) or std::erase_if (for standard containers) over manual iterator-based erase loops. Cleaner and less error-prone.

// ❌ WRONG - manual erase loop
for (auto it = items.begin(); it != items.end();) {
  if (it->IsExpired()) {
    it = items.erase(it);
  } else {
    ++it;
  }
}

// ✅ CORRECT
base::EraseIf(items, [](const auto& item) { return item.IsExpired(); });
// or for std containers:
std::erase_if(items, [](const auto& item) { return item.IsExpired(); });

Use base::span at API Boundaries Instead of const std::vector&

Prefer base::span<const T> over const std::vector<T>& for function parameters that only read data. Spans are lightweight, non-owning views that accept any contiguous container (std::vector, base::HeapArray, C arrays, base::FixedArray), making APIs more flexible.

// ❌ WRONG - forces callers to use std::vector
void ProcessBuffer(const std::vector<uint8_t>& data);

// ✅ CORRECT - accepts any contiguous container
void ProcessBuffer(base::span<const uint8_t> data);

This is especially important for byte buffer APIs where the data source may be a std::vector, base::HeapArray, or a static array.


Use base::FixedArray Over std::vector for Known-Size Runtime Allocations

When the size is known at creation but not at compile time, use base::FixedArray. It avoids heap allocation for small sizes and communicates immutable size.

// ❌ WRONG - vector suggests dynamic resizing
std::vector<uint8_t> out(size);

// ✅ CORRECT - size is fixed after construction
base::FixedArray<uint8_t> out(size);

Use base::HeapArray<uint8_t> for Fixed-Size Byte Buffers

When you need an owned byte buffer that won't be resized after creation, use base::HeapArray<uint8_t> instead of std::vector<unsigned char> or std::vector<uint8_t>. HeapArray communicates that the size is fixed, provides bounds-checked indexing, and converts easily to base::span.

// ❌ WRONG - vector implies the buffer may grow
std::vector<unsigned char> dat_buffer(size);
ProcessBuffer(dat_buffer.data(), dat_buffer.size());

// ✅ CORRECT - HeapArray communicates fixed-size semantics
auto dat_buffer = base::HeapArray<uint8_t>::WithSize(size);
ProcessBuffer(dat_buffer.as_span());

Use HeapArray::Uninit(size) for performance-sensitive paths where zero-initialization is unnecessary.

Note: When interfaces (e.g., Mojo, Rust FFI) require std::vector, you may need to keep using std::vector at those boundaries, but prefer HeapArray for internal buffer management.


Use base::ToVector for Range-to-Vector Conversions

Use base::ToVector(range) instead of manual copy patterns when converting a range to a std::vector. It handles reserve() and iteration automatically, and supports projections.

// ❌ WRONG - manual reserve + copy + back_inserter
std::vector<unsigned char> buffer;
buffer.reserve(sizeof(kStaticData) - 1);
std::copy_n(kStaticData, sizeof(kStaticData) - 1,
            std::back_inserter(buffer));

// ✅ CORRECT - base::ToVector
auto buffer = base::ToVector(base::span(kStaticData).first<sizeof(kStaticData) - 1>());

// ✅ CORRECT - with projection
auto names = base::ToVector(items, &Item::name);

Prefer Contiguous Containers Over Linked Lists

Never use std::list for pure traversal — poor cache locality. Use std::list only when stable iterators or frequent mid-container insert/remove is required. Prefer std::vector with reserve() for known sizes.


Use std::optional Instead of Sentinel Values

Never use empty string "", -1, or other magic values as sentinels for "no value". Use std::optional<T>.

// ❌ WRONG - "" as sentinel for "no custom title"
void SetCustomTitle(const std::string& title);  // "" means "unset"

// ✅ CORRECT - explicit optionality
void SetCustomTitle(std::optional<std::string> title);  // nullopt means "unset"

Use .emplace() for std::optional Initialization Clarity

When engaging a std::optional member, prefer .emplace() for clarity about the intent.

// Less clear
elapsed_timer_ = base::ElapsedTimer();

// ✅ CORRECT - explicit engagement intent
elapsed_timer_.emplace();

Return std::optional Instead of bool + Out Parameter

When a function needs to return a value that may or may not exist, use std::optional<T> instead of returning bool with an out parameter.

// ❌ WRONG
bool GetHistorySize(int* out_size);

// ✅ CORRECT
std::optional<int> GetHistorySize();

Use constexpr for Compile-Time Constants

Constants defined in anonymous namespaces should use constexpr instead of const when the value is known at compile time. Place constants inside the component's namespace.

// ❌ WRONG
namespace {
const int kMaxRetries = 3;
}

// ✅ CORRECT
namespace brave_stats {
namespace {
constexpr int kMaxRetries = 3;
}  // namespace
}  // namespace brave_stats

Use Raw String Literals for Multiline Strings

When embedding multiline strings (JavaScript, SQL, etc.), use raw string literals (R"()") instead of escaping each line.

// ❌ WRONG
const char kScript[] =
    "(function() {\n"
    "  let x = 1;\n"
    "})();";

// ✅ CORRECT
const char kScript[] = R"(
  (function() {
    let x = 1;
  })();
)";

Don't Pass Primitive Types by const Reference

Primitive types (int, bool, float, pointers) should be passed by value, not by const reference. Passing by reference adds unnecessary indirection.

// ❌ WRONG
void ProcessItem(const int& id, const bool& enabled);

// ✅ CORRECT
void ProcessItem(int id, bool enabled);

Don't Add DISALLOW_COPY_AND_ASSIGN in New Code

The DISALLOW_COPY_AND_ASSIGN macro is deprecated. Explicitly delete the copy constructor and copy assignment operator instead.

// ❌ WRONG
class MyClass {
 private:
  DISALLOW_COPY_AND_ASSIGN(MyClass);
};

// ✅ CORRECT
class MyClass {
 public:
  MyClass(const MyClass&) = delete;
  MyClass& operator=(const MyClass&) = delete;
};

Declare Move Operations as noexcept

When defining custom move constructors/assignment operators for structs used in std::vector, declare them noexcept. Without noexcept, std::vector falls back to copying during reallocations.

// ❌ WRONG
Topic(Topic&&) = default;

// ✅ CORRECT
Topic(Topic&&) noexcept = default;
Topic& operator=(Topic&&) noexcept = default;

Avoid std::optional<T>& References

Never pass std::optional<T>& as a function parameter. It's confusing and can cause hidden copies. Take by value if storing, or use base::optional_ref<T> for non-owning optional references.

// ❌ WRONG - confusing, hidden copies
void Process(const std::optional<std::string>& value);

// ✅ CORRECT - take by value if storing
void Process(std::optional<std::string> value);

// ✅ CORRECT - use base::optional_ref for non-owning optional references
void Process(base::optional_ref<const std::string> value);

Short-Circuit on Non-HTTP(S) URLs

In URL processing code (shields, debouncing, content settings), add an early return for non-HTTP/HTTPS URLs. This prevents wasting time on irrelevant schemes and avoids edge cases.

// ✅ CORRECT - early exit
bool ShouldDebounce(const GURL& url) {
  if (!url.SchemeIsHTTPOrHTTPS())
    return false;
  // ...
}

Don't Narrow Integer Types in Setters or Parameters

Setter and function parameter types must match the underlying field type. Accepting a narrower type (e.g., uint32_t when the field is uint64_t) silently truncates values. This is especially dangerous in security-sensitive code like wallet/crypto transactions.

// ❌ WRONG - parameter narrower than field, silent truncation
class Transaction {
  uint64_t invalid_after_ = 0;
  void set_invalid_after(uint32_t value) { invalid_after_ = value; }
};

// ✅ CORRECT - types match
class Transaction {
  uint64_t invalid_after_ = 0;
  void set_invalid_after(uint64_t value) { invalid_after_ = value; }
};

Deprecate Prefs Before Removing Them

When removing a preference that was previously stored in user profiles, first deprecate the pref (register it for clearing) in one release before fully removing it. This ensures the old value is cleared from existing profiles.


Don't Modify Production Code Solely to Accommodate Tests

Test-specific workarounds should not affect production behavior. Use test infrastructure like kHostResolverRules command line switches in SetUpCommandLine instead of adding production code paths only needed for tests.

Only flag this rule when you are certain the code exists solely for tests. Clear signals include CHECK_IS_TEST(), #if defined(UNIT_TEST), _for_testing suffixes, or comments explicitly mentioning test support. Do NOT flag legitimate production logic such as handling empty/null/default values, reset paths, or cleanup behavior — these are normal defensive coding patterns, not test accommodations. When uncertain, do not flag.

Exception: Thin ForTesting() accessors that expose internalized features (e.g., base::Feature) are acceptable. These keep the feature internalized while providing a clean way for tests to reference it, and do not affect production behavior.


Use url::kStandardSchemeSeparator Instead of Hardcoded "://"

When constructing URLs, use url::kStandardSchemeSeparator instead of the hardcoded string "://". This is more maintainable and consistent with Chromium conventions.

// ❌ WRONG
std::string url = scheme + "://" + host + path;

// ✅ CORRECT
std::string url = base::StrCat({url::kHttpsScheme,
                                url::kStandardSchemeSeparator,
                                host, path});

Use base::DoNothing() for No-Op Callbacks

Use base::DoNothing() instead of empty lambdas when a no-op callback is needed. It is the Chromium-idiomatic way and is more readable.

// ❌ WRONG - empty lambda
service->DoAsync([](const std::string&) {});

// ✅ CORRECT
service->DoAsync(base::DoNothing());

Use base::StrAppend Over += base::StrCat

When appending to an existing string, use base::StrAppend(&str, {...}) instead of str += base::StrCat({...}). StrCat creates a temporary string that is then copied; StrAppend appends directly to the target, avoiding unnecessary allocation.

// ❌ WRONG - temporary string then copy
result += base::StrCat({kOpenTag, "\n", "=== METADATA ===\n"});

// ✅ CORRECT - append directly
base::StrAppend(&result, {kOpenTag, "\n", "=== METADATA ===\n"});

Use base::Reversed() for Reverse Iteration

Prefer base::Reversed() with range-based for loops over explicit reverse iterators. Always add a comment explaining why reverse order is needed.

// ❌ WRONG - explicit reverse iterators
for (auto it = history.crbegin(); it != history.crend(); ++it) {
  ProcessEntry(*it);
}

// ✅ CORRECT - base::Reversed with comment
// Process newest entries first to prioritize recent content.
for (const auto& entry : base::Reversed(history)) {
  ProcessEntry(entry);
}

Use absl::StrFormat Over base::StringPrintf

Prefer absl::StrFormat for formatted string construction. base::StringPrintf is being deprecated in favor of absl::StrFormat.

// ❌ WRONG - deprecated
std::string msg = base::StringPrintf("Error %d: %s", code, desc.c_str());

// ✅ CORRECT
std::string msg = absl::StrFormat("Error %d: %s", code, desc);

Use base::saturated_cast for Safe Numeric Conversions

When converting between integer types, use base::saturated_cast<TargetType>() combined with .value_or(default) for safe, concise conversion of optional numeric values.

// ❌ WRONG - manual null-check and static_cast
if (value.has_value()) {
  result = static_cast<uint64_t>(*value);
}

// ✅ CORRECT - safe saturated cast with value_or
result = base::saturated_cast<uint64_t>(value.value_or(0));

Use std::ranges Algorithms Over Manual Loops

Prefer C++20 std::ranges::any_of, std::ranges::all_of, std::ranges::find_if over manual for-loops with break conditions. The ranges versions are more concise and readable.

// ❌ WRONG - manual loop
bool found = false;
for (const auto& item : items) {
  if (item.IsExpired()) {
    found = true;
    break;
  }
}

// ✅ CORRECT - ranges algorithm
bool found = std::ranges::any_of(items,
    [](const auto& item) { return item.IsExpired(); });

Guard substr() with Size Check

Only call substr() when the content actually exceeds the limit. For content within the limit, use the original string to avoid unnecessary memory allocation and copying.

// ❌ WRONG - always creates a substring
std::string truncated = content.substr(0, max_length);

// ✅ CORRECT - only substr when needed
const std::string& truncated = (content.size() > max_length)
    ? content.substr(0, max_length)
    : content;

Use base::expected<T, E> Over Optional + Error Out-Parameter

When a function can fail and needs to communicate error details, use base::expected<T, E> instead of std::optional<T> with a separate error out-parameter. This bundles success and error into a single return value.

// ❌ WRONG - separate error out-parameter
std::optional<Result> Parse(const std::string& input, std::string* error);

// ✅ CORRECT - base::expected bundles both
base::expected<Result, std::string> Parse(const std::string& input);

Use base::MakeFixedFlatMap for Static Enum-to-String Mappings

For compile-time constant mappings between enums and strings, use base::MakeFixedFlatMap. It provides compile-time verification and is more maintainable than switch statements or runtime-built maps.

// ❌ WRONG - runtime map
const std::map<ActionType, std::string> kActionNames = {
    {ActionType::kSummarize, "summarize"},
    {ActionType::kRewrite, "rewrite"},
};

// ✅ CORRECT - compile-time fixed flat map
constexpr auto kActionNames = base::MakeFixedFlatMap<ActionType, std::string_view>({
    {ActionType::kSummarize, "summarize"},
    {ActionType::kRewrite, "rewrite"},
});

Use base::JSONReader::ReadDict for JSON Dictionary Parsing

When parsing a JSON string expected to be a dictionary, use base::JSONReader::ReadDict() which returns std::optional<base::Value::Dict> directly, instead of base::JSONReader::Read() followed by manual GetIfDict() extraction.

// ❌ WRONG - manual extraction
auto value = base::JSONReader::Read(json_str);
if (!value || !value->is_dict()) return;
auto& dict = value->GetDict();

// ✅ CORRECT - direct dict parsing
auto dict = base::JSONReader::ReadDict(json_str);
if (!dict) return;

Pass-by-Value for Sink Parameters (Google Style)

Per Google C++ Style Guide, use pass-by-value for parameters that will be moved into the callee (sink parameters) instead of T&&. The caller uses std::move() either way, and pass-by-value is simpler.

// ❌ WRONG - rvalue reference parameter
void SetName(std::string&& name) { name_ = std::move(name); }

// ✅ CORRECT - pass by value
void SetName(std::string name) { name_ = std::move(name); }

Annotate Obsolete Pref Migration Entries with Dates

When adding preference migration code that removes deprecated prefs, annotate the entry with the date it was added. This makes it easy to identify and clean up old migration code later.

// ❌ WRONG - no context for when this was added
profile_prefs->ClearPref(kOldFeaturePref);

// ✅ CORRECT - annotated with date
profile_prefs->ClearPref(kOldFeaturePref);  // Added 2025-01 (safe to remove after ~3 releases)

Use base::FindOrNull() for Map Lookups

Use base::FindOrNull() instead of the manual find-and-check-end pattern for map lookups. It's more concise and less error-prone.

// ❌ WRONG - verbose find + check
auto it = metric_configs_.find(metric_name);
if (it == metric_configs_.end()) {
  return nullptr;
}
return &it->second;

// ✅ CORRECT
return base::FindOrNull(metric_configs_, metric_name);

Use base::Extend for Appending Ranges to Vectors

Use base::Extend(target, source) instead of manual insert(end, begin, end) for appending one collection to another.

// ❌ WRONG - verbose
accelerator_list.insert(accelerator_list.end(),
    brave_accelerators.begin(), brave_accelerators.end());

// ✅ CORRECT
base::Extend(accelerator_list, base::span(kBraveAcceleratorMap));

Use base::test::ParseJson and base::ExpectDict* in Tests

Use base::test::ParseJson() for parsing JSON in tests, and base::test::* utilities from base/test/values_test_util.h for asserting dict contents. These are more readable and produce better error messages than manual JSON parsing.

// ❌ WRONG - manual JSON parsing in tests
auto value = base::JSONReader::Read(json_str);
ASSERT_TRUE(value);
ASSERT_TRUE(value->is_dict());
auto* name = value->GetDict().FindString("name");
ASSERT_TRUE(name);
EXPECT_EQ(*name, "test");

// ✅ CORRECT - test utilities
auto dict = base::test::ParseJsonDict(json_str);
EXPECT_THAT(dict, base::test::DictHasValue("name", "test"));

Use kOsAll for Cross-Platform Feature Flags

When registering feature flags in about_flags.cc that should be available on all platforms, use kOsAll instead of listing individual platform constants.

// ❌ WRONG - listing platforms individually
{"brave-my-feature", ..., kOsDesktop | kOsAndroid}

// ✅ CORRECT - use kOsAll
{"brave-my-feature", ..., kOsAll}

Workaround Code Must Have Tracking Issues

Any temporary workaround or hack code must reference a tracking issue with a TODO(https://github.com/brave/brave-browser/issues/<id>) comment explaining when and why it can be removed. Workarounds without tracking issues become permanent technical debt.

This rule does NOT apply to permanent design decisions. If a comment explains why an alternative API or approach was not used due to a known limitation, and the current code is the intended long-term solution (not something to revisit later), it is not a workaround -- it is a design rationale comment and does not need a tracking issue.

// ❌ WRONG - unexplained workaround
// HACK: skip validation for now
if (ShouldSkipValidation()) return;

// ✅ CORRECT - tracked workaround with TODO
// TODO(https://github.com/brave/brave-browser/issues/12345): Remove
// this workaround once upstream fixes the validation race condition.
if (ShouldSkipValidation()) return;

// ✅ ALSO CORRECT - permanent design decision, no TODO needed
// FooApi::Connect() relies on the receiver sending Ack messages,
// but the JS bindings don't implement this protocol. Use an explicit
// timer instead.
base::OneShotTimer idle_timer_;

Use Named Constants for JSON Property Keys

When accessing JSON object properties in C++, define named constants for the key strings rather than using inline string literals. This prevents typos and makes refactoring easier.

// ❌ WRONG - inline string literals
auto* name = dict.FindString("display_name");
auto* url = dict.FindString("endpoint_url");

// ✅ CORRECT - named constants
constexpr char kDisplayName[] = "display_name";
constexpr char kEndpointUrl[] = "endpoint_url";
auto* name = dict.FindString(kDisplayName);
auto* url = dict.FindString(kEndpointUrl);

Never Return std::string_view from Functions That Build Strings

Do not return std::string_view from a function that constructs or concatenates a string internally. The view would point into a temporary string's buffer and become a dangling reference after the function returns. Return std::string or std::optional<std::string> instead.

// ❌ WRONG - dangling reference to temporary
std::string_view BuildUrl(std::string_view host) {
  std::string url = base::StrCat({"https://", host, "/api"});
  return url;  // url destroyed, view dangles!
}

// ✅ CORRECT - return by value
std::string BuildUrl(std::string_view host) {
  return base::StrCat({"https://", host, "/api"});
}

Prefer constexpr int Over Single-Value Enums

When a constant is just a single numeric value, use constexpr int rather than creating a single-value enum. Enums are for sets of related values.

// ❌ WRONG - enum for a single value
enum { kBravePolicySource = 10 };

// ✅ CORRECT - constexpr int
constexpr int kBravePolicySource = 10;

Use base::FilePath for File Path Parameters

Parameters representing file system paths should use base::FilePath instead of std::string. This provides type safety, simplifies call sites, and makes APIs self-documenting.

// ❌ WRONG - generic string for a path
std::string GetProfileId(const std::string& profile_path);

// ✅ CORRECT - domain-specific type
std::string GetProfileId(const base::FilePath& profile_path);

Explicitly Assign Enum Values When Conditionally Compiling Out Members

When conditionally compiling out enum values behind a build flag, explicitly assign numeric values to remaining members. This prevents value shifts that break serialization, persistence, or IPC.

// ❌ WRONG - values shift when kTalk is compiled out
enum class SidebarItem {
  kBookmarks,
#if BUILDFLAG(ENABLE_BRAVE_TALK)
  kTalk,
#endif
  kHistory,  // value changes depending on build flag!
};

// ✅ CORRECT - explicit values prevent shifts
enum class SidebarItem {
  kBookmarks = 0,
#if BUILDFLAG(ENABLE_BRAVE_TALK)
  kTalk = 1,
#endif
  kHistory = 2,
};

Name All Function Parameters in Header Declarations

Always name function parameters in header declarations, especially when types alone are ambiguous. Match the parameter names used in the .cc file.

// ❌ WRONG - ambiguous parameters
void OnSubmitSignedExtrinsic(std::optional<std::string>,
                             std::optional<std::string>);

// ✅ CORRECT - named parameters
void OnSubmitSignedExtrinsic(std::optional<std::string> transaction_hash,
                             std::optional<std::string> error_str);

Struct Members: No Trailing Underscores

Plain struct members should not have trailing underscores. The trailing underscore convention is for class member variables, not struct fields.

// ❌ WRONG
struct ContentSite {
  std::string name_;
  int percentage_;
};

// ✅ CORRECT
struct ContentSite {
  std::string name;
  int percentage;
};


Don't Introduce New Uses of Deprecated APIs

When an API is marked deprecated, never introduce new uses. Check headers for deprecation notices before using unfamiliar APIs.

Reviewer note: To flag a violation of this rule, you MUST read the actual header file that declares the API and confirm a deprecation notice exists in the current source tree. Do not rely on memory or training data — APIs change across chromium upgrades and assumptions about what is or isn't deprecated are frequently wrong.

// ❌ WRONG - base::Hash deprecated for 6+ years
uint32_t hash = base::Hash(str);

// ✅ CORRECT - use the recommended replacement
uint32_t hash = base::FastHash(base::as_byte_span(str));

Default-Initialize POD-Type Members in Headers

Plain old data (POD) type members in structs and classes declared in headers must have explicit default initialization. Uninitialized POD members lead to undefined behavior when read before being written.

// ❌ WRONG
struct TopicArticle {
  int id;
  double score;
};

// ✅ CORRECT
struct TopicArticle {
  int id = 0;
  double score = 0.0;
};

Prefer std::string_view Over const char* for Parameters

Use std::string_view instead of const char* for function parameters that accept string data. std::string_view is more flexible (accepts std::string, const char*, string literals) and carries size information.

// ❌ WRONG
std::string_view GetDomain(const char* env_from_switch);

// ✅ CORRECT
std::string_view GetDomain(std::string_view env_from_switch);

⚠️ std::string_view Class Members — Know When They're Safe

Storing std::string_view as a class member is dangerous in general because the referenced data must outlive the object. However, there are well-established safe patterns:

Safe — no need to flag:

  • Members initialized from inline constexpr char[] constants (e.g., pref keys like kMyPrefName), kFeatureName string literals, or other compile-time string constants with static storage duration
  • Members initialized from string literals directly ("some_string")
  • Members in short-lived stack objects where the caller's string clearly outlives the object

Dangerous — flag these:

  • Members initialized from std::string temporaries or function return values
  • Members in long-lived objects (singletons, services) initialized from non-static strings
  • Members where the constructor accepts std::string_view and the caller might pass a temporary
// ✅ SAFE - pref keys are inline constexpr with static lifetime
class MyMetrics {
  std::string_view histogram_name_;  // initialized from kHistogramName
  std::string_view pref_path_;       // initialized from prefs::kMyPref
};

// ❌ DANGEROUS - caller could pass a temporary
class Config {
  std::string_view name_;  // who owns the underlying string?
  Config(std::string_view name) : name_(name) {}  // dangles if temp passed
};

When reviewing: Check what the string_view member is actually initialized from before flagging. Pref key constants and string literals have static lifetime and are safe.


Use base::circular_deque Over std::deque

Always use base::circular_deque (or base::queue/base::stack) instead of std::deque (or std::queue/std::stack). The base versions have consistent memory usage across platforms and save code size. See Chromium container guidelines.

// ❌ WRONG - platform-dependent behavior
std::deque<int> items;
std::queue<int> pending;
std::stack<int> history;

// ✅ CORRECT
base::circular_deque<int> items;
base::queue<int> pending;
base::stack<int> history;

Note: base::circular_deque does not maintain stable iterators during mutations. Use std::list if stable iterators with constant-time insert/remove are required.


Use size_t for Sizes, Counts, and Indices

Use size_t for object sizes, allocation sizes, element counts, array offsets, and vector indices. This prevents unnecessary casts with STL APIs. Public APIs should use size_t even if internals optimize with uint32_t. See Chromium C++ style guide.

// ❌ WRONG - unnecessary casts
int count = static_cast<int>(vec.size());
for (int i = 0; i < vec.size(); ++i) { ... }

// ✅ CORRECT
size_t count = vec.size();
for (size_t i = 0; i < vec.size(); ++i) { ... }

Use Transparent Comparisons for base::flat_map String Lookups

base::flat_map and base::flat_set support transparent comparisons, enabling lookups with std::string_view or const char* without constructing temporary std::string objects. This avoids unnecessary heap allocations on lookups.

// ❌ WRONG - operator[] constructs a temporary std::string
base::flat_map<std::string, int> map;
int val = map["key"];  // temporary std::string("key") created

// ✅ CORRECT - find() uses transparent comparison, no temporary
base::flat_map<std::string, int> map;
auto it = map.find("key");  // no temporary string created

For base::flat_set of std::unique_ptr, use base::UniquePtrComparator for transparent lookups by raw pointer. See Chromium container guidelines.


Don't Rely on Implicit const char*string_view for Non-Null-Terminated Data

When constructing std::string_view from a byte buffer or reinterpret_cast'd pointer, always pass the size explicitly. Implicit string_view(const char*) calls strlen(), which causes undefined behavior if the data is not null-terminated.

This commonly occurs when migrating from (const char* src, size_t src_len) APIs to string_view APIs — dropping the size parameter silently changes the semantics.

std::vector<uint8_t> data = GetBytes();

// ❌ WRONG - implicit strlen() on potentially non-null-terminated data
base::ReadUnicodeCharacter(
    reinterpret_cast<const char*>(data.data()), &i, &code_point);

// ✅ CORRECT - base::as_string_view handles the conversion safely
base::ReadUnicodeCharacter(
    base::as_string_view(data), &i, &code_point);

String Usage in Rust/C++ FFI

Pick FFI string types by data flow (borrow vs. own) and payload size (standard vs. large). Rust's String/Vec and C++'s std::string/std::vector use different allocators and memory layouts, so the wrong choice causes hidden copies, lifetime bugs, or container overhead. Sections below cover each case with examples.

Rust → C++ read-only text — &str (maps to rust::Str)

Zero-copy. Rust retains ownership; C++ reads as a view. Use for logging, printing, parsing.

// ❌ WRONG - forces C++ container overhead for a borrow
unsafe extern "C++" {
    fn log_event(name: &CxxString);
}

// ✅ CORRECT - zero-copy borrow
unsafe extern "C++" {
    fn log_event(name: &str);
}
void LogEvent(rust::Str name) {
  // Treat rust::Str as std::string_view — read only, do not store.
  VLOG(1) << base::RustStrToStringView(name);
}

C++ → Rust read-only text — &str (maps to rust::Str)

Zero-copy. Rust receives a native &str it can search, slice, or print immediately. Use for validation, key lookup.

// ❌ WRONG - bridge type leaks into idiomatic Rust code
fn is_valid_key(key: &CxxString) -> bool;

// ✅ CORRECT - native &str on the Rust side
fn is_valid_key(key: &str) -> bool {
    KNOWN_KEYS.contains(&key)
}
bool valid = ffi::is_valid_key(key_string_view);  // implicit conversion

Rust → C++ ownership, standard text — String (maps to rust::String)

Container move; no text copy. C++ owns the rust::String for the scope of the call. Deep-copy into std::string to preserve past the function's return — never store rust::String as a class member.

// ✅ Return owned text by value
fn build_report() -> String {
    format!("report: {}", compute())
}
// ❌ WRONG - bridge type held past FFI call; ties C++ class to Rust
// allocator and bridge layout.
class Reporter {
  rust::String last_report_;
};
void Reporter::Refresh() {
  last_report_ = ffi::build_report();
}

// ✅ CORRECT - deep-copy into std::string at the boundary
class Reporter {
  std::string last_report_;
};
void Reporter::Refresh() {
  rust::String r = ffi::build_report();
  last_report_.assign(r.data(), r.length());
}

Rust → C++ ownership, MASSIVE payload — Box<Vec<u8>> (maps to rust::Box<rust::Vec<uint8_t>>)

Zero-copy. Heap pointer moves to C++; C++ wraps bytes in std::string_view or base::span. Rust's allocator cleans up when Box drops. Use for file buffers, JSON blobs, streaming payloads.

// ❌ WRONG - container copy through rust::String layout
fn produce_payload() -> String;

// ✅ CORRECT - pointer move, zero-copy
fn produce_payload() -> Box<Vec<u8>>;
void Consume(rust::Box<rust::Vec<uint8_t>> buf) {
  base::span<const uint8_t> bytes(buf);
  // ...consume bytes. Rust deallocator runs when buf drops.
}

C++ → Rust ownership, standard text — String (maps to rust::String)

Deep copy into Rust heap. C++ builds text, copies into rust::String, drops its local copy. Rust gains un-aliased control of an idiomatic container.

// ❌ WRONG - borrowing CxxString into long-lived Rust state ties Rust's
// lifetime to a C++ container.
fn store_name(name: &CxxString);

// ✅ CORRECT - C++ deep-copies at the call site
fn store_name(name: String);
std::string computed = base::StrCat({prefix, suffix});
ffi::store_name(rust::String(computed));  // deep copy into Rust heap

C++ → Rust ownership, MASSIVE payload — UniquePtr<CxxString>

Zero-copy pointer move. Allocation context stays C++-side; cxx invokes the C++ destructor when Rust drops the UniquePtr. Use for ingesting large blobs.

// ❌ WRONG - forces deep copy of large buffer
fn ingest(blob: String);

// ✅ CORRECT - zero-copy pointer move
fn ingest(blob: UniquePtr<CxxString>);
auto blob = std::make_unique<std::string>(LoadLargeFile());
ffi::ingest(std::move(blob));  // ownership transfers to Rust

Collections of strings

Same borrow-vs-own split. &CxxVector<CxxString> is almost never the right answer.

Rust → C++ read-only array — &[&str]

Zero-copy, zero-allocation. No C++ container overhead.

// ❌ WRONG - C++ container overhead for a borrow
unsafe extern "C++" {
    fn process(keys: &CxxVector<CxxString>);
}

// ✅ CORRECT
unsafe extern "C++" {
    fn process(keys: &[&str]);
}

C++ → Rust ownership, large — UniquePtr<CxxVector<CxxString>>

Container lifetime owned by Rust; strings readable without per-element conversion.

fn ingest(items: UniquePtr<CxxVector<CxxString>>);
auto items = std::make_unique<std::vector<std::string>>(BuildItems());
ffi::ingest(std::move(items));

C++ → Rust ownership, small / ergonomic — Vec<String> (deep copy)

Highest safety. Fully decouples from C++ memory on receipt.

fn ingest_small(items: Vec<String>);