fix(cpp): -> on a value of one of the project's classes follows its operator->, like rocksdb's FileSystemPtr and MultiGetContext::Range::Iterator (#2735)

C++ reads `x->m` on a class value as `x.operator->()->m`, and applies
`->` again to a class it returns. Only a library holder's `->` and a
container iterator's (`std::vector<T>::iterator`) were read. A call
through a project iterator or smart-pointer wrapper was guessed by the
method's name:
- rocksdb's `fs_->NewWritableFile(…)` on a `FileSystemPtr`, whose
  `operator->` returns a `std::shared_ptr<FileSystem>`, reached
  `FaultInjectionTestFS`;
- `writable_file_->Truncate(…)` on an `FSWritableFilePtr` reached the
  legacy `WritableFile::Truncate`;
- `iter->lkey->user_key()` through `MultiGetContext::Range::Iterator`,
  whose `KeyContext* operator->()` is a `KeyContext*`, reached
  `BlobCountingIterator::user_key`.

Both a member chain's `->` and a `->` call on a declared receiver end in
cppIteratorTarget. For a type that is no container's iterator, it now
falls back to cppOperatorTarget:
- what the class's `operator->` returns, through the pointer it is, or
  with `->` applied again to a class it is: a library holder, or another
  of these;
- else what its `operator*` returns;
- the return type is read where the class declares it, and the class's
  own template parameters are bound to the type's written arguments
  (cppTemplateBound): `UnownedPtr<DB>`'s `T* operator->()` is a `DB*`;
- a parameter the type doesn't bind names no particular class.

A log-only census on main bdf288a7 found 304 rocksdb and 42 protobuf
dereferences of a project class with an `operator->` that gave no
element. Among the rocksdb calls, 77 were bare-name guesses and 56 were
unlinked.

Kit A/B against bdf288a7:

| repo | removed | added | retargeted | same target, now typed |
|---|---|---|---|---|
| rocksdb | 2 | 49 | 158 | 89 |
| protobuf | 0 | 0 | 7 | 3 |
| leveldb, fmt, redis | identical | | | |

- Retargeted:
  - FaultInjectionTestFS → FileSystem (57), WritableFile → FSWritableFile
    (32), and TruncatedRangeDelIterator through UnownedPtr (16);
  - LookupKey and GetContext through MultiGetContext's iterator;
  - SequentialFile and RandomAccessFile → FSSequentialFile and
    FSRandomAccessFile;
  - protobuf's `map_field_->` calls from WeakRepeatedPtrField to the map
    field bases;
  - overloads (DB::Put, PinnableSlice) chosen on the now-known class.
- Removed: `iter->timestamp->assign(…)` on a `std::string*`, which had
  reached `autovector::assign`.
- Added: Status::PermitUncheckedError, FileSystem::OptimizeFor…,
  Cleanable::RegisterCleanup, and gtest's ParamGeneratorInterface through
  linked_ptr.
- About 40 changed sites read by hand were all right.

Mutations: dropping the fallback, the operator* fallback, the second
`->` or the template binding each fail the new tests. Accepting an
inexact class, found only as the sole one of its name, survives; the
guard stays, as in the member-chain matcher.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-21 08:16:26 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 4e35e0cb78
commit 0c28480576
3 changed files with 214 additions and 3 deletions
+57 -3
View File
@@ -14610,16 +14610,70 @@ function cppArrowTarget(
/**
* The element a container's iterator of the type `iterator` points at, read
* from the container's template arguments (cppArrowTarget). Null for any
* other type, and for an element a class template's own `begin()` or
* from the container's template arguments (cppArrowTarget), or what `->` on
* a value of one of the project's classes reaches (cppOperatorTarget). Null
* for any other type, and for an element a class template's own `begin()` or
* `operator*` writes with its template parameter.
*/
function cppIteratorTarget(iterator: CppWrittenType, at: UnresolvedRef, context: ResolutionContext): CppWrittenType | null {
const container = CPP_ITERATOR_OF.exec(cppWithoutTypeKeywords(iterator.text))?.[1];
const element = container ? cppRangeElementOf({ ...iterator, text: container }, at, context, 1) : null;
const element = container ? cppRangeElementOf({ ...iterator, text: container }, at, context, 1) : cppOperatorTarget(iterator, at, context);
return element && (element.scope === iterator.scope || !isCppTemplateParameterType(element)) ? element : null;
}
/**
* What `->` on a value of one of the project's classes of the type `type`
* reaches, as C++ reads `x->m`: what the class's `operator->` returns,
* through the pointer it is, or with `->` applied again to a class it is (a
* library holder's held type, or another of these); else what its
* `operator*` returns. The return type is read where the class declares it,
* with the class's own template parameters bound to the arguments `type` is
* written with (cppTemplateBound). rocksdb's `MultiGetContext::Range::Iterator`
* declares `KeyContext* operator->()`, its `UnownedPtr<DB>` a `T*
* operator->()`, a `DB*`, and its `FileSystemPtr` returns a
* `std::shared_ptr<FileSystem>`. Null when the class declares neither, or
* what it returns can't be read.
*/
function cppOperatorTarget(type: CppWrittenType, at: UnresolvedRef, context: ResolutionContext, hops = 0): CppWrittenType | null {
const written = cppWithoutTypeKeywords(type.text);
if (hops > 2 || !written || isCppPointerType(written) || isCppLibraryType(written, context)) return null;
const found = cppClassWritten(written, type, at, context);
if (!found?.exact || !CPP_CLASS_KINDS.has(found.node.kind)) return null;
const arrow = cppMethodsOf(found.node, 'operator->', at, context);
const returned = cppReturnTypeOf(arrow.length > 0 ? arrow : cppMethodsOf(found.node, 'operator*', at, context), context);
const bound = returned && cppTemplateBound(returned, found.node, { ...type, text: written }, at, context);
if (!bound) return null;
if (arrow.length === 0) return { ...bound, text: bound.text.replace(/&+\s*$/, '').trim() };
return cppPointee(bound) ?? cppOperatorTarget(bound, at, context, hops + 1);
}
/**
* The type `returned`, which the class template `cls` writes, with `cls`'s
* own template parameters bound to the arguments of `type`, a specialization
* of it: the `T*` of `UnownedPtr<T>` is a `DB*` for an `UnownedPtr<DB>`. An
* alias the class declares (`pointer`) is followed first, and an argument is
* looked up where `type` is written. A type of a class that is no template
* comes back as written; null for a parameter `type` doesn't bind.
*/
function cppTemplateBound(returned: CppWrittenType, cls: Node, type: CppWrittenType, at: UnresolvedRef, context: ResolutionContext): CppWrittenType | null {
const params = cppOwnTemplateParameters(cls, context);
if (params.length === 0) return returned;
const args = cppTemplateArguments(type.text) ?? [];
const followed = cppFollowWrittenAliases(returned, at, context) ?? returned;
const core = followed.text.replace(/\b(?:const|volatile|typename)\b|[*&]/g, ' ').trim();
const index = params.indexOf(core);
if (index >= 0) {
const arg = args[index]?.trim();
return arg ? { ...type, text: followed.text.replace(new RegExp(`\\b${core}\\b`), arg) } : null;
}
let text = followed.text;
params.forEach((param, i) => {
const arg = args[i]?.trim();
if (param && arg) text = text.replace(new RegExp(`\\b${param}\\b`, 'g'), arg);
});
return { ...followed, text };
}
/**
* What dereferencing a value of the type `type` gives, for `*x` and `x->`:
* an array's first element, what a pointer points at or a holder holds