This document describes the coding conventions used in the WinGet CLI codebase. It is a companion to:
CONTRIBUTING.md— workflow and process guidancedoc/Developing.md— build, test, and localization instructions
The codebase is primarily C++/WinRT. Brief notes for .NET (C#) components appear at the end.
All source files must conform to the rules in .editorconfig:
- Line endings: CRLF
- Encoding: UTF-8
- Final newline: required
- Trailing whitespace: must be trimmed
- YAML files (
.yml/.yaml): 2-space indentation - Markdown files (
.md): tab indentation
Configure your editor to apply these settings on save, or run a check before committing.
The codebase uses Allman style (also known as BSD style): the opening brace of every block goes on its own new line, at the same indentation level as the statement that introduced it.
namespace AppInstaller::CLI::Workflow
{
void SomeWorkflowTask(Execution::Context& context)
{
if (condition)
{
DoSomething();
}
else if (otherCondition)
{
DoSomethingElse();
}
else
{
Fallback();
}
for (const auto& item : items)
{
Process(item);
}
switch (value)
{
case SomeEnum::First:
HandleFirst();
break;
case SomeEnum::Second:
HandleSecond();
break;
default:
THROW_HR(E_UNEXPECTED);
}
}
}Key points:
elseandelse ifgo on their own line after the closing}— never on the same line as}.caselabels inside aswitchare not indented relative to theswitchkeyword; they sit at the same level.- Trivial single-expression bodies (simple getters, forwarding constructors, one-liner lambdas) may be written inline:
Use judgment: if the body is anything more than a single expression, use the full Allman form.
bool IsTerminated() const { return m_isTerminated; } SomeClass(std::string name) : m_name(std::move(name)) {}
| Element | Convention | Example |
|---|---|---|
| Types (classes, structs, enums, type aliases) | PascalCase |
ExecutionContext, ContextFlag |
| Functions and methods | PascalCase |
GetErrorCode(), IsTerminated() |
| Local variables and parameters | camelCase |
installerType, packageId |
| Non-static member variables | m_ prefix + camelCase |
m_name, m_flags |
| Static member variables | s_ prefix + camelCase |
s_disabledReason |
| Namespaces | PascalCase |
AppInstaller::CLI::Workflow |
| Macros | AICLI_ or WINGET_ prefix, ALL_CAPS |
AICLI_TERMINATE_CONTEXT, WINGET_CATCH_STORE |
| Enum members | PascalCase |
ContextFlag::InstallerTrusted |
Follow standard .NET naming conventions (PascalCase for public members, camelCase for local variables and private fields). StyleCop is configured in src/stylecop.json.
The codebase uses the Windows Implementation Library (WIL) for HRESULT-based error handling. Prefer WIL macros over manual if (FAILED(hr)) checks:
// Throw on failure — use in code where exceptions are acceptable
THROW_IF_FAILED(SomeWin32OrComApi());
THROW_HR(E_UNEXPECTED);
THROW_HR_IF(E_POINTER, ptr == nullptr);
THROW_HR_MSG(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), "Stage %d is not valid here", stage);
// Return the HRESULT on failure — use in COM methods or HRESULT-returning functions
RETURN_IF_FAILED(SomeWin32OrComApi());
// Log without propagating — use for non-fatal side effects
LOG_IF_FAILED(CleanupTempFiles());Prefer the most specific macro. For example, THROW_HR_IF is cleaner than if (condition) { THROW_HR(...); }.
Inside workflow functions (functions that take Execution::Context& context), use the AICLI_* family of macros rather than throwing:
void WorkflowTask(Execution::Context& context)
{
// Guard against an already-terminated context at the top of every task
AICLI_RETURN_IF_TERMINATED(context);
// Terminate the context and return from the current function
if (failed)
{
AICLI_TERMINATE_CONTEXT(HRESULT_VALUE);
}
// Terminate but return a specific value (useful in non-void helpers)
// AICLI_TERMINATE_CONTEXT_RETURN(HRESULT_VALUE, returnValue);
}AICLI_TERMINATE_CONTEXT records the file and line of the failure, sets the context's termination HRESULT, and returns from the current function. Do not throw exceptions out of workflow functions — use context termination instead.
Every switch statement over an enum or integer must have an explicit default branch. That branch must either:
-
Terminate with
E_UNEXPECTEDif reaching it indicates a programming error (an enum value was added but the switch was not updated, or the caller passed an invalid value):switch (installerType) { case InstallerTypeEnum::Exe: /* ... */ break; case InstallerTypeEnum::Msi: /* ... */ break; default: THROW_HR(E_UNEXPECTED); }
-
Have a clearly intentional and documented fallback when a default behavior is genuinely correct:
switch (result) { case ConfigurationTestResult::Positive: return Resource::StringId::ConfigPositive; case ConfigurationTestResult::Negative: return Resource::StringId::ConfigNegative; default: return Resource::StringId::Empty(); // Unknown/not yet tested }
The same principle applies outside of switches: use THROW_HR_IF(E_UNEXPECTED, condition) to assert runtime invariants that should never be violated:
THROW_HR_IF(E_UNEXPECTED, entries.size() != expectedCount);Do not leave an empty default: or an empty else branch that silently discards an unexpected case. Silent failures are harder to debug than explicit ones.
Never use C-style casts. C-style casts (e.g., (int)value) bypass the type system silently. Use the named C++ cast operators instead:
| Situation | Use |
|---|---|
| Safe numeric or enum conversions | static_cast<T>(value) |
| Reinterpreting pointer/integer bytes | reinterpret_cast<T>(value) |
Removing const (rare; justify in a comment) |
const_cast<T>(value) |
| Downcasting via virtual dispatch | dynamic_cast<T>(value) |
| WinRT interface conversion | .as<T>() from C++/WinRT |
Converting ToIntegral for enums |
Use the project helper ToIntegral(enumValue) where available |
std::move() casts a value to an rvalue reference so its resources can be transferred rather than copied. Use it only where ownership transfer is clearly intended.
- Passing to a constructor or function that takes by value or
&&when you no longer need the source:context.Add<Data::Manifest>(std::move(manifest)); m_items.push_back(std::move(item));
- Initializing members from constructor parameters:
MyClass(std::string name) : m_name(std::move(name)) {}
-
On
returnstatements. Named Return Value Optimization (NRVO) can eliminate the copy/move entirely, but only if you return the variable directly.std::move()on a return statement suppresses NRVO and can result in an extra move that would not otherwise occur:// ✗ Suppresses NRVO std::string BuildResult() { return std::move(result); } // ✓ Allows NRVO std::string BuildResult() { return result; }
-
On trivially copyable types (
int,HRESULT, raw pointers, enums, etc.). Moving these is no cheaper than copying, andstd::move()just adds noise. -
On
constobjects. The move constructor cannot be selected for aconstobject; the call silently falls back to a copy. -
On an object you still need after the call. After a move, the source is in a valid but unspecified state. Accessing it without re-assignment is undefined behavior.
All user-visible strings must be added to the English resource file:
src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw
Do not edit any file under Localization/Resources/<locale>/; those files are owned by Microsoft's localization pipeline and will be overwritten automatically.
Every new or modified string entry must include a <comment> element that gives translators enough context. This is especially important for:
- Short or single-word values (column headers, status labels) where the word has multiple meanings
- Technical jargon that may have a different colloquial meaning in other languages
- Strings with placeholders — document what each
{0},{1}, etc. represents
See Localization in doc/Developing.md for an example.
The PowerShell modules (src/PowerShell) and configuration tests are written in C#. In addition to the file header and naming guidance above:
- StyleCop Analyzers are configured in
src/stylecop.json. Build warnings from StyleCop must not be suppressed without justification. - Follow standard .NET exception handling — avoid swallowing exceptions silently.
- Match the namespace structure of the surrounding project (e.g.,
Microsoft.WinGet.Client.Engine.Commands).