tool/cpp_front.py is a second front end written against the state model, not the rules: it reads a compile database with libclang (Homebrew LLVM) and writes the JSON that `pipe check --model` takes. Every rule, baseline and gate works on it unchanged. Mapping, per project `enum class` (identity from clang's resolved AST, so two enums both named QueryIntent are never confused): - a reference that produces a value (assignment, atomic store, argument, return, initialiser) is an emission; a cast from an int is an indirect one; - a `case`, `==` or `!=` is an observation; - a busy enumerator written to the method's OWN state, followed by a top-level try whose catch neither writes state nor rethrows, is a swallowed failure; - sources the build does not compile are recorded as unresolved, so findings naming them drop a confidence step. One rule-side change, optional and Dart-neutral: an EmissionSite may name its stateType. rendered-not-emitted attributed indirect emissions by owner (a bloc owns one state type), so a cast to one enum made "never emitted" unprovable for every other enum in the same C++ class. With stateType set, the suppression is scoped to that enum. Dart output is still byte identical on the fixture and all four dogfood apps. Run on ~/work/tools/yams (375 translation units from build/debug, 0 clang errors, about two minutes): 118 state enums, 82 findings. A grep-based hand check of 13 found 12 factually correct - e.g. the IPC connection FSM never enters ImplState::ReadingHeader, the downloader never reports ProgressStage::Verifying though three CLI commands render it, ExtractScopeType qualifiers are parsed but never acted on - and one false positive (a nested JSON-parse fallback taken for a stuck busy state), now excluded by requiring the method's own state and a top-level try. Bugs found on the way and fixed: ccache/compiler wrappers passed as inputs, relative -I paths resolved against the wrong directory (7 TUs silently lost third_party headers), a symlinked path putting every TU "outside" the project, and an empty compile database producing an empty "clean" model with exit 0 - all now fail loudly or are handled. test/cpp_front_test.dart runs the front end on test/fixtures/cpp_state with positive and negative controls for each rule; skipped without LLVM. 138 tests pass.
104 lines
2.3 KiB
C++
104 lines
2.3 KiB
C++
// Fixture for tool/cpp_front.py: each case is a positive or negative control
|
|
// for a rule run on the C++ state model (see test/cpp_front_test.dart).
|
|
|
|
namespace fx {
|
|
|
|
// Ghost is compared against but never produced: rendered-not-emitted.
|
|
enum class Phase { Idle, Starting, Running, Failed, Ghost };
|
|
|
|
// Produced, never compared or switched on: emitted-not-rendered.
|
|
enum class Unread { A, B };
|
|
|
|
// Y is compared against and produced only through a cast from an int, so the
|
|
// rule must suppress itself rather than call the branch dead.
|
|
enum class Casted { X, Y };
|
|
|
|
void mayThrow();
|
|
|
|
class Engine {
|
|
public:
|
|
// Negative: the catch resolves the busy state.
|
|
void startGuarded() {
|
|
phase_ = Phase::Starting;
|
|
try {
|
|
mayThrow();
|
|
phase_ = Phase::Running;
|
|
} catch (...) {
|
|
phase_ = Phase::Failed;
|
|
}
|
|
}
|
|
|
|
// Positive: busy state set, then a top-level try whose catch swallows the
|
|
// failure, so Starting can outlive it (loading-not-reset).
|
|
void startSwallowed() {
|
|
phase_ = Phase::Starting;
|
|
try {
|
|
mayThrow();
|
|
phase_ = Phase::Running;
|
|
} catch (...) {
|
|
}
|
|
}
|
|
|
|
// Negative: a nested try is a local fallback, not the method's failure path.
|
|
void startNested() {
|
|
phase_ = Phase::Starting;
|
|
if (ready_) {
|
|
try {
|
|
mayThrow();
|
|
} catch (...) {
|
|
}
|
|
}
|
|
phase_ = Phase::Running;
|
|
}
|
|
|
|
bool ghost() const { return phase_ == Phase::Ghost; }
|
|
|
|
bool idle() const {
|
|
switch (phase_) {
|
|
case Phase::Idle:
|
|
return true;
|
|
case Phase::Starting:
|
|
case Phase::Running:
|
|
case Phase::Failed:
|
|
return false;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void setUnread() {
|
|
unread_ = Unread::A;
|
|
unread_ = Unread::B;
|
|
}
|
|
|
|
void load(int n) { casted_ = static_cast<Casted>(n); }
|
|
bool isY() const { return casted_ == Casted::Y; }
|
|
|
|
private:
|
|
bool ready_ = false;
|
|
Phase phase_ = Phase::Idle;
|
|
Unread unread_ = Unread::A;
|
|
Casted casted_ = Casted::X;
|
|
};
|
|
|
|
// Negative: `ev.stage` is a field of a local event object handed to a callback,
|
|
// not state Reporter holds, so its try cannot leave Reporter stuck.
|
|
struct Event {
|
|
Phase stage;
|
|
};
|
|
void notify(const Event&);
|
|
|
|
class Reporter {
|
|
public:
|
|
void run() {
|
|
Event ev;
|
|
ev.stage = Phase::Starting;
|
|
notify(ev);
|
|
try {
|
|
mayThrow();
|
|
} catch (...) {
|
|
}
|
|
}
|
|
};
|
|
|
|
} // namespace fx
|