Skip to content
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.

## Next Release

### Features

#### CPP Rules

- Add `noNewInGtestSetup` AST check flagging `new` expressions anywhere inside a member function named `SetUp`, `SetUpTestSuite`, or `SetUpTestCase` — the fixture-setup hooks GTest calls by name. A raw pointer allocated there needs a matching manual release whose timing depends on a separate teardown hook firing correctly later; prefer an RAII/smart-pointer owner instead. Only a literal `new` lexically inside the setup function's own body is flagged, not calls into other functions. Takes no configuration
- Add `noDeleteInGtestTeardown` AST check flagging `delete`/`delete[]` expressions anywhere inside a member function named `TearDown`, `TearDownTestSuite`, or `TearDownTestCase` — the fixture-teardown hooks GTest calls by name. Manually deleting a raw pointer there is fragile (a `SetUp()` that throws or returns early skips the matching `delete`, and a test body that already freed the pointer causes a double-free); prefer an RAII/smart-pointer owner instead. Only a literal `delete`/`delete[]` lexically inside the teardown function's own body is flagged, not calls into other functions. Takes no configuration

<!-- insertion marker -->
## [0.8.0](https://github.com/repo/owner/releases/tag/0.8.0) - 2026-09-30

Expand Down
10 changes: 10 additions & 0 deletions docs/source/sections/ast_checks/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,14 @@ Available checks
* - :doc:`noThrowParen <no_throw_paren>`
- ``throw(...)`` wrapping the whole thrown expression.
- None
* - :doc:`noDeleteInGtestTeardown <no_delete_in_gtest_teardown>`
- ``delete``/``delete[]`` inside a GTest ``TearDown``/``TearDownTestSuite``/
``TearDownTestCase`` function.
- None
* - :doc:`noNewInGtestSetup <no_new_in_gtest_setup>`
- ``new`` inside a GTest ``SetUp``/``SetUpTestSuite``/``SetUpTestCase``
function.
- None

.. toctree::
:maxdepth: 1
Expand All @@ -107,3 +115,5 @@ Available checks
no_global_using
no_global_using_enum
no_throw_paren
no_delete_in_gtest_teardown
no_new_in_gtest_setup
49 changes: 49 additions & 0 deletions docs/source/sections/ast_checks/no_delete_in_gtest_teardown.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
noDeleteInGtestTeardown
=========================

Flags any ``delete``/``delete[]`` expression appearing anywhere inside a
member function named ``TearDown``, ``TearDownTestSuite``, or
``TearDownTestCase`` — the fixture-teardown hooks GTest calls by name.
Manually deleting a raw pointer there is fragile: if ``SetUp()`` throws or
returns early, the matching ``delete`` in ``TearDown()`` never runs, and if
a test body already frees the pointer, the ``TearDown()`` delete
double-frees it.

.. code-block:: cpp

class FooTest : public ::testing::Test {
protected:
void TearDown() override {
delete ptr_; // Bad — flagged.
}

Foo* ptr_;
};

class BarTest : public ::testing::Test {
protected:
void TearDown() override {
ptr_.reset(); // Good — RAII, not flagged.
}

std::unique_ptr<Bar> ptr_;
};

Only a literal ``delete``/``delete[]`` lexically inside the teardown
function's own body is flagged — a call to some other function that itself
deletes something is not traced into. A declaration with no body (e.g. a
pure-virtual ``TearDown() = 0``) is never flagged, and neither is a free
function of the same name that isn't a member function.

Configuration
--------------

This check takes no configuration.

Disabling
----------

.. code-block:: toml

[cpp]
ast_check_disabled_ids = ["noDeleteInGtestTeardown"]
48 changes: 48 additions & 0 deletions docs/source/sections/ast_checks/no_new_in_gtest_setup.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
noNewInGtestSetup
====================

Flags any ``new`` expression appearing anywhere inside a member function
named ``SetUp``, ``SetUpTestSuite``, or ``SetUpTestCase`` — the
fixture-setup hooks GTest calls by name. A raw pointer allocated there needs
a matching manual release, and whether that release runs depends on a
separate teardown hook firing later — see also
:doc:`noDeleteInGtestTeardown <no_delete_in_gtest_teardown>`.

.. code-block:: cpp

class FooTest : public ::testing::Test {
protected:
void SetUp() override {
ptr_ = new Foo(); // Bad — flagged.
}

Foo* ptr_;
};

class BarTest : public ::testing::Test {
protected:
void SetUp() override {
ptr_ = std::make_unique<Bar>(); // Good — RAII, not flagged.
}

std::unique_ptr<Bar> ptr_;
};

Only a literal ``new`` expression lexically inside the setup function's own
body is flagged — a call to some other function that itself allocates is
not traced into. A declaration with no body (e.g. a pure-virtual
``SetUp() = 0``) is never flagged, and neither is a free function of the
same name that isn't a member function.

Configuration
--------------

This check takes no configuration.

Disabling
----------

.. code-block:: toml

[cpp]
ast_check_disabled_ids = ["noNewInGtestSetup"]
113 changes: 113 additions & 0 deletions src/devops/cpp/ast/checks/no_delete_in_gtest_teardown.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""Flag `delete`/`delete[]` expressions inside a GTest teardown function.

A member function named ``TearDown``, ``TearDownTestSuite``, or
``TearDownTestCase`` — the fixture-teardown hooks GTest calls by name
(``TearDown()`` after every test, the other two once per test suite) — is
reported for any ``delete``/``delete[]`` expression appearing anywhere in
its body. Manually deleting a raw pointer there is exactly the kind of
lifetime bookkeeping RAII/smart pointers exist to make unnecessary, and it's
especially fragile in a teardown hook: if ``SetUp()`` throws or returns
early, the matching ``delete`` in ``TearDown()`` never runs, and if a test
body already frees the pointer, the ``TearDown()`` delete double-frees it.
Prefer ``std::unique_ptr``/``std::shared_ptr`` (or another RAII owner) for
anything the fixture needs to release, so its lifetime doesn't depend on a
teardown hook running at all.

Only a literal ``delete``/``delete[]`` lexically inside the teardown
function's own body is flagged — a call to some other function that itself
deletes something is not traced into.

To disable this check for a project set::

[cpp]
ast_check_disabled_ids = ["noDeleteInGtestTeardown"]
"""

from __future__ import annotations

import clang.cindex as clang

from devops.cpp.ast.base import Check, Diagnostic

# GTest calls these fixture methods by name; no base-class relationship is
# required by the framework itself, so none is checked here either.
_TEARDOWN_NAMES = frozenset(
(
"TearDown",
"TearDownTestSuite",
"TearDownTestCase",
)
)


class NoDeleteInGtestTeardown(Check):
"""Flag `delete`/`delete[]` expressions inside a GTest teardown function."""

id = "noDeleteInGtestTeardown"

def visit(self, cursor: clang.Cursor, filename: str) -> list[Diagnostic]:
"""Flag any delete-expression inside a GTest teardown function body.

Parameters
----------
cursor: clang.Cursor
The AST node currently being visited.
filename: str
Path of the file being checked.

Returns
-------
list[Diagnostic]
One diagnostic per `delete`/`delete[]` expression found inside
`cursor`'s body, if `cursor` is a GTest teardown function;
otherwise an empty list.

"""
if cursor.kind != clang.CursorKind.CXX_METHOD:
return []
if cursor.spelling not in _TEARDOWN_NAMES:
return []
if not cursor.is_definition():
return []

return [
self._make_diagnostic(delete_expr, cursor.spelling, filename)
for delete_expr in cursor.walk_preorder()
if delete_expr.kind == clang.CursorKind.CXX_DELETE_EXPR
]

def _make_diagnostic(
self, delete_expr: clang.Cursor, function_name: str, filename: str
) -> Diagnostic:
"""Build the diagnostic for one flagged delete-expression.

Parameters
----------
delete_expr: clang.Cursor
The `CXX_DELETE_EXPR` cursor to report.
function_name: str
Name of the enclosing teardown function, for the message.
filename: str
Path of the file being checked.

Returns
-------
Diagnostic
The diagnostic for `delete_expr`.

"""
tokens = [t.spelling for t in delete_expr.get_tokens()]
spelling = "delete[]" if tokens[1:2] == ["["] else "delete"
loc = delete_expr.location
return Diagnostic(
file=filename,
line=loc.line,
column=loc.column,
message=(
f"do not use '{spelling}' inside GTest teardown function "
f"'{function_name}()' — prefer an RAII/smart-pointer owner "
"whose lifetime doesn't depend on teardown running"
),
check_id=self.id,
severity="style",
)
111 changes: 111 additions & 0 deletions src/devops/cpp/ast/checks/no_new_in_gtest_setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"""Flag `new` expressions inside a GTest setup function.

A member function named ``SetUp``, ``SetUpTestSuite``, or ``SetUpTestCase``
— the fixture-setup hooks GTest calls by name (``SetUp()`` before every
test, the other two once per test suite) — is reported for any ``new``
expression appearing anywhere in its body. A raw pointer allocated there
needs a matching manual release, and whether that release actually runs
depends on a separate teardown hook firing later (see also
:doc:`noDeleteInGtestTeardown </sections/ast_checks/no_delete_in_gtest_teardown>`)
— if `SetUp()` itself throws partway through, or a later `SetUp()` call in
the same fixture leaks the previous allocation, the memory is never freed.
Prefer ``std::unique_ptr``/``std::shared_ptr`` (or another RAII owner) so
the allocation's lifetime is tied to the fixture object itself rather than
to a hook running at the right time.

Only a literal ``new`` expression lexically inside the setup function's own
body is flagged — a call to some other function that itself allocates is
not traced into.

To disable this check for a project set::

[cpp]
ast_check_disabled_ids = ["noNewInGtestSetup"]
"""

from __future__ import annotations

import clang.cindex as clang

from devops.cpp.ast.base import Check, Diagnostic

# GTest calls these fixture methods by name; no base-class relationship is
# required by the framework itself, so none is checked here either.
_SETUP_NAMES = frozenset(
(
"SetUp",
"SetUpTestSuite",
"SetUpTestCase",
)
)


class NoNewInGtestSetup(Check):
"""Flag `new` expressions inside a GTest setup function."""

id = "noNewInGtestSetup"

def visit(self, cursor: clang.Cursor, filename: str) -> list[Diagnostic]:
"""Flag any new-expression inside a GTest setup function body.

Parameters
----------
cursor: clang.Cursor
The AST node currently being visited.
filename: str
Path of the file being checked.

Returns
-------
list[Diagnostic]
One diagnostic per `new` expression found inside `cursor`'s
body, if `cursor` is a GTest setup function; otherwise an empty
list.

"""
if cursor.kind != clang.CursorKind.CXX_METHOD:
return []
if cursor.spelling not in _SETUP_NAMES:
return []
if not cursor.is_definition():
return []

return [
self._make_diagnostic(new_expr, cursor.spelling, filename)
for new_expr in cursor.walk_preorder()
if new_expr.kind == clang.CursorKind.CXX_NEW_EXPR
]

def _make_diagnostic(
self, new_expr: clang.Cursor, function_name: str, filename: str
) -> Diagnostic:
"""Build the diagnostic for one flagged new-expression.

Parameters
----------
new_expr: clang.Cursor
The `CXX_NEW_EXPR` cursor to report.
function_name: str
Name of the enclosing setup function, for the message.
filename: str
Path of the file being checked.

Returns
-------
Diagnostic
The diagnostic for `new_expr`.

"""
loc = new_expr.location
return Diagnostic(
file=filename,
line=loc.line,
column=loc.column,
message=(
f"do not use 'new' inside GTest setup function "
f"'{function_name}()' — prefer an RAII/smart-pointer owner "
"whose lifetime doesn't depend on a later teardown hook"
),
check_id=self.id,
severity="style",
)
4 changes: 4 additions & 0 deletions src/devops/cpp/ast/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@
)
from devops.cpp.ast.checks.enforce_param_name_for_type import EnforceParamNameForType
from devops.cpp.ast.checks.macro_replacement import MacroReplacement
from devops.cpp.ast.checks.no_delete_in_gtest_teardown import NoDeleteInGtestTeardown
from devops.cpp.ast.checks.no_global_using import NoGlobalUsing
from devops.cpp.ast.checks.no_global_using_enum import NoGlobalUsingEnum
from devops.cpp.ast.checks.no_new_in_gtest_setup import NoNewInGtestSetup
from devops.cpp.ast.checks.no_throw_paren import NoThrowParen
from devops.logger import cpp_check_logger

Expand All @@ -40,8 +42,10 @@
EnforceNoPublicLeadingUnderscore(),
EnforceParamNameForType(),
MacroReplacement(),
NoDeleteInGtestTeardown(),
NoGlobalUsing(),
NoGlobalUsingEnum(),
NoNewInGtestSetup(),
NoThrowParen(),
]

Expand Down
Loading
Loading