diff --git a/CHANGELOG.md b/CHANGELOG.md index 08226636c..cc84e55eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,6 +77,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In C and C++, a header or source file named like a test, such as protocolbuffers/protobuf's `conformance_test.h` and `test_runner.h`, now counts as part of the code that `#include`s it, directly or through other headers. Before, calls from that code into it were dropped, so protobuf's conformance suites' `suite_.ReportFailure(…)` and `RunValidInputTest(…)` calls, and its unit tests' calls to the shared `TestUtil` helpers, linked to nothing. A test file that only tests include is still kept out of reach of the rest of your code. Re-index C and C++ projects after upgrading. - In C++, a class, struct or method now keeps the namespaces and the class it is declared in when the parser misreads something earlier in the file, such as an unknown macro in front of a member or in a class header. Before, everything after the misread code could lose them, or land inside a class it isn't in: in protocolbuffers/protobuf, `FieldDescriptor` was indexed without `google::protobuf::`, and in RocksDB, the `Opts` struct declared inside a cache table class was indexed outside it, so finding a class's base classes and the methods called on its objects fell back to guessing by name. Methods that had come loose from their class are its members again, and classes the misread code had hidden, like leveldb's POSIX file and environment classes, are now indexed. Re-index C++ projects after upgrading. - In C++, a call on a variable, parameter or member declared as a standard-library or other outside type with a lowercase name, like `std::string`, `std::vector`, `std::set` or `absl::flat_hash_set`, no longer links to a project method that only shares the method's name. In google/leveldb, `saved_key_.clear()` on a `std::string saved_key_;` reached `Slice::clear`, and in protocolbuffers/protobuf, `proto.append(…)` on a `std::string` reached an unrelated `LeftoverBuffer::append`. The same goes for `->` through a raw pointer like `std::string* out`, while `->` through a smart pointer, iterator or optional still links to the type it holds. Comments also no longer pass for a variable's declaration, so more calls now link to the method of the type the variable is really declared with. Re-index C++ projects after upgrading. +- In C++, a call on a range-based `for` loop's variable, like `suite->SetVerbose(verbose)` inside `for (ConformanceTestSuite *suite : suites)`, now links to the method of the type the loop declares. Before, the loop's declaration was never read, so the call was matched by the variable's name alone: in RocksDB, `log->file()` on a `log::Writer` went to `BlobLogWriter::file`, and a loop variable named like a member of its class was taken for that member. The variable counts only inside its loop, and loops over `auto` elements resolve as before. Re-index C++ projects after upgrading. - Framework support now switches on when an app's `package.json` sits three or more folders deep, as with an ASP.NET solution's Angular or React app in `src/Web/ClientApp/`, or a React app in a workspace folder like `web/ui/mantine-ui/`. Before, only the project root and the first two folder levels were read, so frameworks such as Angular, React Router, Vue Router, SvelteKit and TanStack Router could go undetected there: an Angular app's routes, its `routerLink` and `router.navigate(…)` navigation and its template's event bindings were missing, so a handler used only from a `(click)` binding had no callers, and a React app's `navigate('/login')` calls didn't reach their route. Re-index projects laid out this way after upgrading. - In a repository that mixes languages, a web framework's naming conventions now link only code written in that framework's own languages. Before, Express's also linked Go and Python code: in etcd a Go method's `*zap.Logger` result type linked to the method itself, and in Harbor a Python `self.jaeger.validate()` call linked to another class's `validate`. Links that cross languages on purpose, like React Native and Expo native modules, Swift and Objective-C, Drupal's `routing.yml`, SvelteKit's `$lib/…` imports in `.ts` files and a Razor page's `@model`, are unchanged. - In Go, an interface that embeds another, like gin's `IRouter` embedding `IRoutes`, now links to it, so `codegraph_explore` and impact show the embedded interface as its base. Before, every embedded interface was dropped. An embedded field written through another package of your project, like `*refresh.Discovery` or `storage.Appender`, or with type arguments, like `List[int]`, now links too. One from the standard library or another module, like `io.Closer` or `sync.Mutex`, links to nothing rather than to a project type that shares its name, and a type-set constraint like `~int | ~float64` is not an embedding. An embedded type written without a package now always links to its own package's type: before, etcd's `Client`, which embeds its own `Lease` interface, was linked to a `Lease` struct in another package. Re-index Go projects after upgrading. diff --git a/__tests__/cpp-range-for-receiver.test.ts b/__tests__/cpp-range-for-receiver.test.ts new file mode 100644 index 000000000..67da5c2f9 --- /dev/null +++ b/__tests__/cpp-range-for-receiver.test.ts @@ -0,0 +1,446 @@ +/** + * A C++ range-based for loop declares the receiver its body calls through. + * + * protocolbuffers/protobuf's conformance runner loops `for + * (ConformanceTestSuite *suite : suites)` and calls `suite->SetVerbose(…)`, + * `suite->RunSuite(…)` and `suite->GetFailureListFlagName()` in the body. + * Receiver inference reads a declaration only when the declared name is + * followed by `;`, `=`, `,`, `)`, `[`, `{` or `(`, so it never read a loop's + * `Type name :` and the calls fell through to a guess by the receiver's name. + * A guess can still land on the right method by a shared word, so these tests + * check how the edge was made (the typed path's confidence), not only where + * it goes. + * + * The loop's variable is in scope in the loop's body only: after the loop, or + * in another function, a receiver of the same name is some other variable. + * A `for` in a comment or a string literal declares nothing, and a colon + * outside a loop's header never ends a declaration: not a bit-field's + * (`unsigned car : 1;`), nor the scope operator's (`void parts::Install()`). + */ +import { describe, it, expect, afterEach } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; +import type { Node } from '../src/types'; + +const roots: string[] = []; +afterEach(() => { + for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true }); +}); + +async function indexed(files: Record): Promise { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-range-for-')); + roots.push(root); + for (const [rel, content] of Object.entries(files)) { + fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); + fs.writeFileSync(path.join(root, rel), content); + } + return CodeGraph.init(root, { index: true }); +} + +function callable(cg: CodeGraph, qualifiedName: string): Node { + const node = [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')].find((n) => n.qualifiedName === qualifiedName); + if (!node) throw new Error(`no function or method ${qualifiedName}`); + return node; +} + +/** `calls` callees of a function or method, as `qualifiedName @confidence`, one per call site. */ +function calls(cg: CodeGraph, caller: string): string[] { + return cg + .getCallees(callable(cg, caller).id) + .filter((r) => r.edge.kind === 'calls') + .map((r) => `${r.node.qualifiedName} @${(r.edge.metadata as { confidence?: number } | undefined)?.confidence}`) + .sort(); +} + +/** protocolbuffers/protobuf's conformance runner, trimmed; `FlagParser` is a decoy `SetVerbose`. */ +const CONFORMANCE = { + 'conformance/suite.h': [ + '#include ', + 'namespace google {', + 'namespace protobuf {', + 'class ConformanceTestRunner {', + ' public:', + ' virtual ~ConformanceTestRunner() = default;', + '};', + 'class ConformanceTestSuite {', + ' public:', + ' void SetVerbose(bool verbose) { verbose_ = verbose; }', + ' std::string GetFailureListFlagName() { return failure_list_flag_name_; }', + ' bool RunSuite(ConformanceTestRunner* runner, std::string* output,', + ' const std::string& filename);', + ' private:', + ' bool verbose_ = false;', + ' std::string failure_list_flag_name_;', + '};', + 'class FlagParser {', + ' public:', + ' void SetVerbose(bool verbose) {}', + '};', + '} // namespace protobuf', + '} // namespace google', + '', + ].join('\n'), + 'conformance/suite.cc': [ + '#include "conformance/suite.h"', + 'namespace google {', + 'namespace protobuf {', + 'bool ConformanceTestSuite::RunSuite(ConformanceTestRunner* runner, std::string* output,', + ' const std::string& filename) {', + ' return true;', + '}', + '} // namespace protobuf', + '} // namespace google', + '', + ].join('\n'), + 'conformance/runner.cc': [ + '#include ', + '#include ', + '#include "conformance/suite.h"', + 'namespace google {', + 'namespace protobuf {', + 'int ForkPipeRunnerMain(int argc, char* argv[], const std::vector& suites) {', + ' bool verbose = false;', + ' for (ConformanceTestSuite *suite : suites) {', + ' std::string failure_list_filename;', + ' for (int arg = 1; arg < argc; ++arg) {', + ' if (strcmp(argv[arg], suite->GetFailureListFlagName().c_str()) == 0) {', + ' failure_list_filename = argv[arg];', + ' }', + ' }', + ' suite->SetVerbose(verbose);', + ' std::string output;', + ' ConformanceTestRunner* runner = nullptr;', + ' verbose = verbose && suite->RunSuite(runner, &output,', + ' failure_list_filename);', + ' }', + ' return 0;', + '}', + '} // namespace protobuf', + '} // namespace google', + '', + ].join('\n'), +}; + +/** Two `Start`s no receiver name below points at, and a class whose member is named like a loop variable. */ +const GARAGE = { + 'garage/vehicle.h': [ + '#include ', + 'namespace garage {', + 'class Engine {', + ' public:', + ' void Start();', + ' int Torque() const;', + '};', + 'class Car {', + ' public:', + ' void Start();', + '};', + 'namespace parts {', + 'class Part {', + ' public:', + ' void Fit();', + '};', + 'void Install();', + '} // namespace parts', + '} // namespace garage', + '', + ].join('\n'), + 'garage/vehicle.cc': [ + '#include "garage/vehicle.h"', + 'namespace garage {', + 'void Engine::Start() {}', + 'int Engine::Torque() const { return 0; }', + 'void Car::Start() {}', + 'void parts::Part::Fit() {}', + '} // namespace garage', + '', + ].join('\n'), + 'garage/garage.h': [ + '#include "garage/vehicle.h"', + 'namespace garage {', + 'class Garage {', + ' public:', + ' void Inspect(const std::vector& engines);', + ' void Service();', + ' private:', + ' Car* car;', + '};', + '} // namespace garage', + '', + ].join('\n'), + 'garage/garage.cc': [ + '#include "garage/garage.h"', + 'namespace garage {', + 'void Garage::Inspect(const std::vector& engines) {', + ' for (Engine* car : engines) car->Start();', + '}', + 'void Garage::Service() {', + ' car->Start();', + '}', + '} // namespace garage', + '', + ].join('\n'), +}; + +describe('a C++ range-based for declares its loop variable', () => { + it('reproduction: protobuf\'s conformance runner calls the suite\'s methods through the typed path', async () => { + const cg = await indexed(CONFORMANCE); + try { + // The guess by the receiver's name lands on the same methods (0.65 / + // 0.7); the loop's declared type makes them certain. + expect(calls(cg, 'google::protobuf::ForkPipeRunnerMain')).toEqual([ + 'google::protobuf::ConformanceTestSuite::GetFailureListFlagName @0.9', + 'google::protobuf::ConformanceTestSuite::RunSuite @0.9', + 'google::protobuf::ConformanceTestSuite::SetVerbose @0.9', + ]); + } finally { + cg.close(); + } + }); + + it('a loop variable that hides a member is the element, not the member', async () => { + const cg = await indexed(GARAGE); + try { + // Reading on past the loop reached the member `Car* car;` in the header. + expect(calls(cg, 'garage::Garage::Inspect')).toEqual(['garage::Engine::Start @0.9']); + } finally { + cg.close(); + } + }); + + it('after the loop, and in another function, a same-named receiver is not the loop\'s variable', async () => { + const cg = await indexed({ + ...GARAGE, + 'garage/fleet.cc': [ + '#include "garage/vehicle.h"', + 'namespace garage {', + 'void Drive(Car* engine, const std::vector& engines) {', + ' for (Engine* engine : engines) {', + ' engine->Start();', + ' }', + ' engine->Start();', + '}', + 'void Wash(Car* engine, const std::vector& engines) {', + ' for (Engine* engine : engines) engine->Start();', + ' engine->Start();', + '}', + '} // namespace garage', + '', + ].join('\n'), + }); + try { + expect(calls(cg, 'garage::Drive')).toEqual(['garage::Car::Start @0.9', 'garage::Engine::Start @0.9']); + // A loop without braces ends with its one statement. + expect(calls(cg, 'garage::Wash')).toEqual(['garage::Car::Start @0.9', 'garage::Engine::Start @0.9']); + // Garage::Service's `car` is the member, though Inspect's loop above it names one `car`. + expect(calls(cg, 'garage::Garage::Service')).toEqual(['garage::Car::Start @0.9']); + } finally { + cg.close(); + } + }); + + it('reads references, pointers, qualified and global-scope types, and a header split over lines', async () => { + const cg = await indexed({ + ...GARAGE, + 'garage/shop.cc': [ + '#include "garage/vehicle.h"', + 'namespace garage {', + 'void ByReference(std::vector& engines) {', + ' for (Engine& e : engines) e.Start();', + '}', + 'void ByConstReference(const std::vector& engines) {', + ' for (const Engine& e : engines) {', + ' if (e.Torque() > 0) {}', + ' }', + '}', + 'void StarOnName(const std::vector& engines) {', + ' for (Engine *e : engines)', + ' e->Start();', + '}', + 'void Qualified(const std::vector& list) {', + ' for (::garage::parts::Part* p : list) p->Fit();', + '}', + 'void SplitHeader(const std::vector& engines) {', + ' for (Engine* first_engine_in_the_list :', + ' engines) {', + ' first_engine_in_the_list->Start();', + ' }', + '}', + '} // namespace garage', + '', + ].join('\n'), + }); + try { + expect(calls(cg, 'garage::ByReference')).toEqual(['garage::Engine::Start @0.9']); + expect(calls(cg, 'garage::ByConstReference')).toEqual(['garage::Engine::Torque @0.9']); + expect(calls(cg, 'garage::StarOnName')).toEqual(['garage::Engine::Start @0.9']); + expect(calls(cg, 'garage::Qualified')).toEqual(['garage::parts::Part::Fit @0.9']); + expect(calls(cg, 'garage::SplitHeader')).toEqual(['garage::Engine::Start @0.9']); + } finally { + cg.close(); + } + }); + + it('a brace in a comment or a string literal does not end the loop', async () => { + const cg = await indexed({ + ...GARAGE, + 'garage/braces.cc': [ + '#include "garage/vehicle.h"', + 'namespace garage {', + 'void Braces(const std::vector& engines) {', + ' for (Engine* e : engines) {', + ' // A comment\'s } is not the loop\'s.', + ' const char* close = "}";', + ' /* nor is } this one', + ' } or this */', + ' char brace = \'}\';', + ' e->Start();', + ' }', + '}', + '} // namespace garage', + '', + ].join('\n'), + }); + try { + expect(calls(cg, 'garage::Braces')).toEqual(['garage::Engine::Start @0.9']); + } finally { + cg.close(); + } + }); + + it('a nested loop\'s call is inside the outer loop\'s body', async () => { + const cg = await indexed({ + ...GARAGE, + 'garage/nested.cc': [ + '#include "garage/vehicle.h"', + 'namespace garage {', + 'void Nested(const std::vector& list) {', + ' for (parts::Part* p : list) {', + ' for (int i = 0; i < 2; ++i) {', + ' if (i) { p->Fit(); }', + ' }', + ' }', + '}', + '} // namespace garage', + '', + ].join('\n'), + }); + try { + expect(calls(cg, 'garage::Nested')).toEqual(['garage::parts::Part::Fit @0.9']); + } finally { + cg.close(); + } + }); +}); + +describe('what declares no loop variable', () => { + it('a `for` in a comment or a string literal', async () => { + const cg = await indexed({ + ...GARAGE, + 'garage/notes.cc': [ + '#include ', + '#include "garage/vehicle.h"', + 'namespace garage {', + 'void Commented(Car* car) {', + ' // for (Engine* car : engines) {', + ' car->Start();', + '}', + 'void Inline(Car* car) {', + ' /* for (Engine* car : engines) { */ car->Start();', + '}', + 'std::string Template(Car* car) {', + ' std::string code = "for (Engine* car : engines) {";', + ' car->Start();', + ' return code + "}";', + '}', + '} // namespace garage', + '', + ].join('\n'), + }); + try { + expect(calls(cg, 'garage::Commented')).toEqual(['garage::Car::Start @0.9']); + expect(calls(cg, 'garage::Inline')).toEqual(['garage::Car::Start @0.9']); + expect(calls(cg, 'garage::Template')).toEqual(['garage::Car::Start @0.9']); + } finally { + cg.close(); + } + }); + + it('a bit-field', async () => { + const cg = await indexed({ + ...GARAGE, + 'garage/lot.h': [ + '#include "garage/vehicle.h"', + 'namespace garage {', + 'struct Slot {', + ' unsigned car : 1;', + '};', + 'class Lot {', + ' public:', + ' void Park();', + ' private:', + ' Car* car;', + '};', + '} // namespace garage', + '', + ].join('\n'), + 'garage/lot.cc': [ + '#include "garage/lot.h"', + 'namespace garage {', + 'void Lot::Park() {', + ' car->Start();', + '}', + '} // namespace garage', + '', + ].join('\n'), + }); + try { + // The member `Car* car;`, not Slot's `unsigned car : 1;` above it. + expect(calls(cg, 'garage::Lot::Park')).toEqual(['garage::Car::Start @0.9']); + } finally { + cg.close(); + } + }); + + it('a name followed by the scope operator', async () => { + const cg = await indexed({ + ...GARAGE, + 'garage/bay.h': [ + '#include "garage/vehicle.h"', + 'namespace garage {', + 'class Bay {', + ' public:', + ' void Open();', + ' void Fit(const std::vector& list);', + ' private:', + ' Car* parts;', + '};', + '} // namespace garage', + '', + ].join('\n'), + 'garage/bay.cc': [ + '#include "garage/bay.h"', + 'namespace garage {', + 'void parts::Install() {}', + 'void Bay::Open() {', + ' parts->Start();', + '}', + 'void Bay::Fit(const std::vector& list) {', + ' for (parts::Part* part : list) parts->Start();', + '}', + '} // namespace garage', + '', + ].join('\n'), + }); + try { + // `void parts::Install()` declares no `parts`; the member does. + expect(calls(cg, 'garage::Bay::Open')).toEqual(['garage::Car::Start @0.9']); + // `parts::Part` declares the loop's `part`; the receiver `parts` is still the member. + expect(calls(cg, 'garage::Bay::Fit')).toEqual(['garage::Car::Start @0.9']); + } finally { + cg.close(); + } + }); +}); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 36365398d..26feedea4 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -8366,13 +8366,109 @@ function normalizeCppTypeName(typeName: string): string | null { // `Type*receiver`, `Type receiver`, etc., REQUIRING a declarator terminator // (`;`, `=`, `,`, `)`, `[`, `{`, `(`, or end-of-line) after the receiver. The // terminator rules out uses like `return receiver->m()` where the preceding -// token is a keyword, not a type. +// token is a keyword, not a type. A range-based for's `Type receiver :` is read +// by cppRangeForDeclaration instead. function buildDeclaratorRegex(escapedReceiver: string): RegExp { return new RegExp( `([A-Za-z_][\\w:]*(?:\\s*<[^;=(){}]+>)?(?:\\s*[*&]+)?)\\s*\\b${escapedReceiver}\\b\\s*(?=[;=,)\\[{(]|$)`, ); } +// A range-based for's declaration, `for (const Foo& name :`, up to the single +// colon (not `::`) that ends it. What may precede the type — `const`, a +// global-scope `::` — is never an initializer, a call or a member access. +const CPP_RANGE_FOR_DECLARATION = /\bfor\s*\(([^;=(){}?."']*?)\b([A-Za-z_]\w*)\s*:(?!:)/g; +// The type a declaration ends with, read as the declarator regex reads it. +const CPP_DECLARED_TYPE_TAIL = /([A-Za-z_][\w:]*(?:\s*<[^;=(){}]+>)?(?:\s*[*&]+)?)\s*$/; + +/** + * The type the range-based for on `line` (comments blanked) declares + * `receiverName` with — `for (ConformanceTestSuite *suite : suites)` — and + * the column of the header's `(`. The declarator regex never reads one: a + * loop's name is followed by its `:`. (Neither is a bit-field, `unsigned car + * : 1;`, which no method is called on.) + */ +function cppRangeForDeclaration(line: string, receiverName: string): { type: string; paren: number } | null { + if (!line.includes('for')) return null; + for (const m of line.matchAll(CPP_RANGE_FOR_DECLARATION)) { + if (m[2] !== receiverName) continue; + const tail = m[1]!.match(CPP_DECLARED_TYPE_TAIL); + return tail ? { type: tail[1]!, paren: m.index! + m[0].indexOf('(') } : null; + } + return null; +} + +/** Where the C or C++ string or character literal opening at `text[start]` closes (the line's end if it doesn't). */ +function cppLiteralEnd(text: string, start: number): number { + for (let i = start + 1; i < text.length; i++) { + if (text[i] === '\\') i++; + else if (text[i] === text[start]) return i; + } + return text.length; +} + +/** + * Is column `useColumn` of line `useLine` inside the body of the `for` whose + * header opens at `lines[line][paren]`? A range-based for's variable is in + * scope there and nowhere else: after the loop, or in a later function, the + * same name is some other variable. The body runs to the `}` closing a braced + * body, or to the end of its one statement — taken to be the first `;` or + * closing `}` at its own depth, so an `else` after it is not counted. + * Comments and literals are skipped, and a `for` inside one (commented-out + * code, a code generator's template) has no body. + */ +function cppForBodyEncloses(lines: readonly string[], line: number, paren: number, useLine: number, useColumn: number): boolean { + let phase: 'before' | 'header' | 'gap' | 'block' | 'statement' = 'before'; + let depth = 0; + let comment = false; + for (let l = line; l <= useLine && l < lines.length; l++) { + const text = lines[l]!; + const end = l === useLine ? Math.min(useColumn, text.length) : text.length; + for (let c = 0; c < end; c++) { + const ch = text[c]!; + if (comment) { + if (ch === '*' && text[c + 1] === '/') { + comment = false; + c++; + } + continue; + } + if (ch === '/' && text[c + 1] === '/') break; + if (ch === '/' && text[c + 1] === '*') { + comment = true; + c++; + continue; + } + // (A `'` after a digit separates digits: `1'000'000`.) + if (ch === '"' || (ch === '\'' && !/\d/.test(text[c - 1] ?? ''))) { + c = cppLiteralEnd(text, c); + continue; + } + if (phase === 'before') { + if (l === line && c === paren) { + phase = 'header'; + depth = 1; + } + continue; + } + if (phase === 'header') { + if (ch === '(') depth++; + else if (ch === ')' && --depth === 0) phase = 'gap'; + continue; + } + if (phase === 'gap') { + if (/\s/.test(ch)) continue; + phase = ch === '{' ? 'block' : 'statement'; + } + if (ch === '(' || ch === '[' || ch === '{') depth++; + else if (ch === ')' || ch === ']') depth--; + else if (ch === '}' && --depth <= 0) return false; + else if (ch === ';' && depth <= 0 && phase === 'statement') return false; + } + } + return phase !== 'before' && phase !== 'header'; +} + /** What C++ receiver inference made of a receiver's declared type. */ interface CppReceiverDeclaration { /** @@ -8614,8 +8710,20 @@ function inferCppReceiverType( noteCppDeclaration(found, declaratorMatch[1]!, inCallerScope); return cppDeclaredType(declaratorMatch[1]!, normalized, inCallerScope, ref, context, found); } - } else if (found && cppRebindsReceiver(line, escapedReceiver)) { - found.shadowed = true; + } else { + // `for (ConformanceTestSuite *suite : suites)`, when the call is in the + // loop's body. A loop over `auto` elements has no initializer to read, + // so the scan goes on as for an `auto` local whose initializer is unreadable. + const loopVar = cppRangeForDeclaration(line, receiverName); + if (loopVar && cppForBodyEncloses(lines, i, loopVar.paren, callLineIndex, ref.column)) { + const normalized = normalizeCppTypeName(loopVar.type); + if (normalized && normalized !== 'auto') { + const inCallerScope = isCppCallersDeclaration(ref.filePath, i + 1, ref, context); + noteCppDeclaration(found, loopVar.type, inCallerScope); + return cppDeclaredType(loopVar.type, normalized, inCallerScope, ref, context, found); + } + } + if (found && cppRebindsReceiver(line, escapedReceiver)) found.shadowed = true; } }