Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
# Переход к определению и «Найти ссылки» для методов, найденных через вывод типа

> Рабочие заметки сессии. Контекст: почему F12 не работает на `ИмеетТип`
> в `Ожидаем.Что(Переменная).ИмеетТип(...)` (библиотека `asserts`), и можно ли
> сделать так, чтобы такие вызовы попадали в «Найти ссылки» и «Переименовать».
> Это **разбор и замысел**, кода пока не написано.

## 1. Исходная проблема

В модуле с `#использовать asserts`:

```bsl
Ожидаем.Что(Переменная).ИмеетТип("ДанныеСоставнойФормы");
```

- **Подсказка (hover)** на `ИмеетТип` показывается, и подробная. ✅
- **Переход к определению (F12)** на `ИмеетТип` молча ничего не делает. ❌

## 2. Почему подсказка есть, а перехода нет

Оба механизма зовут один и тот же `referenceResolver.findReference(...)` и получают
одну и ту же ссылку — расходятся в обработке результата.

Ссылку на `ИмеетТип` отдаёт **`PlatformMemberReferenceFinder` (@Order(200))**
(`references/PlatformMemberReferenceFinder.java:59-77`). Он находит метод через тип,
который вернул `Ожидаем.Что(Переменная)` — то есть через `TypeService.memberAt(...)`,
и заворачивает результат в служебный символ **`PlatformMemberSymbol`**. Этот символ:

- реализует `Symbol` напрямую — это **не** `SourceDefinedSymbol`;
- в дерево символов не входит (`accept()` пустой, `types/symbol/PlatformMemberSymbol.java:70-72`);
- несёт `MemberDescriptor` (имя, сигнатуры, описание).

Дальше:

- **Подсказка** подбирает построитель текста по классу символа → для
`PlatformMemberSymbol` построитель есть → показывает подсказку. ✅
- **Переход к определению** (`providers/DefinitionProvider.java:112-116`) отсеивает
всё, что не `SourceDefinedSymbol` (проверка `Reference::isSourceDefinedSymbolReference`).
`PlatformMemberSymbol` эту проверку не проходит → возвращается пустой список →
переход молча не срабатывает. ❌

Почему ссылка не приходит из индекса (где как раз лежат `SourceDefinedSymbol`):
`ReferenceIndexReferenceFinder` (@Order(40), идёт первым) находит только то, что
записал в индекс обходчик `ReferenceIndexFiller`. А вызовы методов на типе
произвольного выражения он не записывает — он не выводит типы. Поэтому до `ИмеетТип`
доходит только механизм №200 (через вывод типа).

## 3. Важная находка: исходный символ уже подложен

У `MemberDescriptor` есть поле `@Nullable Symbol sourceSymbol`
(`types/model/MemberDescriptor.java:66`) и методы `getSourceSymbol()`/`withSourceSymbol()`.

И главное — для метода класса OneScript-библиотеки это поле **уже заполнено**.
`types/oscript/OScriptModuleMembersProvider.java:261-294`, метод
`toMemberDescriptor(MethodSymbol)`:

```java
return MemberDescriptor.method(method.getName(), purpose, List.of(signature))
.withSourceSymbol(method); // ← настоящий MethodSymbol подложен
```

`PlatformMemberReferenceFinder` кладёт весь `member.descriptor()` в
`PlatformMemberSymbol` (строка 68), а `getDescriptor()` открыт. Значит
`platformMemberSymbol.getDescriptor().getSourceSymbol()` доступен как есть.

**Вывод:** для перехода к определению вся нужная информация уже есть в момент
запроса. Не хватает только того, чтобы `DefinitionProvider` её прочитал.

### Самая маленькая правка для F12 (часть общей задачи)

В `DefinitionProvider.findLocationLinks`: если ссылка не `SourceDefinedSymbol`, но это
`PlatformMemberSymbol`, у которого `descriptor.getSourceSymbol()` — это
`SourceDefinedSymbol`, то строить переход на него. Методы платформы (без исходного
символа) остаются только с подсказкой — это правильно. Индекс и систему типов не
трогает.

## 4. Настоящая цель: «Найти ссылки» и «Переименовать»

Подсказка и переход точечны. Хочется, чтобы **«Найти все ссылки» на `MethodSymbol`
метода `ИмеетТип`** находил все места вызова вида `Ожидаем.Что(...).ИмеетТип(...)`.
И чтобы «Переименовать» шло следом.

Как это работает сейчас (`providers/ReferencesProvider.java:61-81`):

```
найти символ под курсором → referenceIndex.getReferencesTo(symbol)
```
Comment on lines +85 to +87

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a language tag to the fenced block to satisfy markdownlint.

Line 85 uses an unlabeled fenced block; add a language (for example, text) to resolve MD040.

Suggested patch
-```
+```text
 resolve символ под курсором → referenceIndex.getReferencesTo(symbol)
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 85-85: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
@.session-notes/goto-definition-and-find-references-for-type-inferred-members.md
around lines 85 - 87, The fenced code block in the markdown note is missing a
language tag and triggers markdownlint MD040. Update the unlabeled fence in the
referenced note to include a language label such as text, keeping the existing
content and formatting otherwise unchanged.

Source: Linters/SAST tools


`ReferenceIndex.getReferencesTo` (`references/ReferenceIndex.java:76-102`) — это
**просто поиск по ключу** записи `(mdoRef, moduleType, scopeName, kind, name)`,
без всякого вывода типа. Значит, чтобы «Найти ссылки» нашёл места вызова через
цепочку, записи о них должны лежать в индексе под ключом метода `ИмеетТип`.

Ключ для метода OneScript-класса уже определён и используется
(`ReferenceIndexFiller.processLibraryClassAccessCall`):
`mdoRef = <путь .os>, moduleType = OScriptClass, name`. Не записывается только то,
что обходчик не может определить владельца без вывода типа приёмника.

## 5. Тупик №1: выводить тип на запросе — НЕ выдерживает масштаба

Замысел «не записывать в индекс, а при запросе „Найти ссылки“ прогнать вывод типа по
всем местам с нужным именем» отклонён. Если 10 000 файлов вызывают `ИмеетТип` через
`Ожидаем.Что()`, то это 10 000 выводов типа **на каждый** запрос «Найти ссылки» —
секунды/минуты на разовое действие. Так нельзя.

## 6. Тупик №2 (мой ранний неверный довод)

Сначала я отговаривал от вывода типов в обходчике под предлогом «вывод типа дорогой».
Это неверная посылка: 10 000 приёмников придётся вывести в любом случае. Вопрос не
«платить или нет», а **когда** платить — один раз при наполнении индекса (понемногу,
по одному документу) или каждый раз на запросе. Первое — единственный вариант, который
держит масштаб.

## 7. Вывод: записывать заранее, ключ — по типу

Владельца метода **нужно** определить заранее и положить в индекс как часть ключа,
иначе «Найти ссылки» перестаёт быть быстрым поиском. Раз без вывода типа владельца
`.ИмеетТип` не узнать — **вывод типа делается при наполнении индекса**, не на запросе.

### Из чего складывается ключ

Хранить запись не как `(mdoRef владельца, …)`, а как **`(тип приёмника, имя метода)`** —
это ровно то, что вывод типа отдаёт напрямую, без обратного перевода «тип → путь
модуля». На запросе `getReferencesTo(S)` для метода OneScript-класса определяем
тип `T`, которому принадлежит символ `S` (обратный указатель «путь → тип» уже есть —
`GlobalScopeProvider.indexModuleType`, см. `OScriptModuleMembersProvider.java:155`),
и ищем по `(T, имя)`. Симметрично записи, по-прежнему быстро.

### Что реально стоит труда — не скорость запроса, а поддержание индекса в актуальном виде

Сейчас индекс дёшев и верен потому, что ключ берётся из **текста самого документа**
(`ОбщийМодуль.Метод` — путь прямо из имени в коде) и не зависит от содержимого других
документов. Ключ «по типу приёмника» этот порядок нарушает. Источники устаревания:

1. **Порядок при первой загрузке.** Документ A может обойтись раньше, чем
зарегистрирован тип класса B → вывод типа вернёт «неизвестно» → место вызова не
попадёт в индекс.
→ Лечится полным обходом по `ServerContextPopulatedEvent`
(`context/events/ServerContextPopulatedEvent.java` — выходит после загрузки и
обработки всех документов; к этому моменту типы устаканились).

2. **Правка самого документа A.** Перезаписываем исходящие вызовы методов в A —
типы B уже готовы. Это нынешняя работа обходчика плюс вывод типа по доступам к
методам.

3. **Правка определения типа B.** ВАЖНО (поправка к раннему выводу): типы, на чьи
методы ссылаются места вызова, — это в первую очередь **код самого проекта**, а
его правят постоянно. Поэтому «потерпеть устаревание, библиотеки меняются редко» —
неверно: пересчёт по этому случаю **обязателен**, не откладывается.

Но не всякая правка B делает запись в A устаревшей. Ключ — `(тип приёмника T, имя M)`,
где `M` берётся из текста A.

- **Правка тела метода B** (бо́льшая часть правок) — ключи не меняет. Видимый
снаружи набор методов не затронут. (Методы и так берутся «по запросу» из живого
дерева символов: `OScriptModuleMembersProvider.registerMemberSource(ref, () ->
collectMembers)`.)
- **Переименование метода в B** (`ИмеетТип`→`Equals`) — запись в A перезаписывать
**не надо**, ровно как у обычных вызовов: имя `M` в ключе берётся из текста A.
Поиск по новому символу строит ключ `(T,"equals")` → запись A `(T,"имееттип")`
не совпадает → она верно исключается; старого символа больше нет, вызов в A стал
висячим, и индекс правильно ни с чем его не связывает. Исправляется само.
- **Смена возвращаемого типа метода** (например, у `Что()`) — ЕДИНСТВЕННОЕ, что
реально делает запись устаревшей: `TypeOf(Ожидаем.Что(X))` стал другим →
записанный `T` в A устарел.

→ Обязательный, но **узкий** пересчёт: «у типа B сменился возвращаемый тип или
набор видимых методов → перезаписать только те документы, у чьих записей тип
приёмника принадлежит B». Список таких документов даёт сам индекс (он ведь и
построен по типу приёмника) — перезапись точечная, а не «весь проект». Признак
запуска — дешёвый отпечаток видимого набора (имена + сигнатуры + возвращаемые типы),
сверяемый при перерегистрации источника методов.

По возможностям — по-разному:
- **Переименовать** — устаревание недопустимо (пропустить место вызова = молча
сломать код) → пересчёт по смене видимого набора обязателен;
- **Найти ссылки** — только чтение, подсказочно → можно временно потерпеть
неполноту до следующей правки файла, если режем объём первого шага.

## 8. Итог

- «Найти ссылки» и «Переименовать» через вывод типа — **только запись в индекс
заранее, с ключом по типу приёмника**. Вариант «выводить на запросе» отпадает.
- Неизбежная цена — один вывод типа на каждое место вызова метода при первой загрузке
и при правке самого документа (пропорционально числу доступов к методам; для 10 000
файлов — заметный, но разовый расход; стоит включать настройкой).
- Главная инженерная работа — **поддержание индекса в актуальном виде при правке
типов**, а не скорость запроса.

## 9. Предлагаемый объём (черновик плана)

1. Ключ записи `(тип приёмника, имя метода)` в модели записей
(`references/model/Symbol`, хранилища записей).
2. Вывод типа по доступам к методам в `ReferenceIndexFiller` + полный обход на
`ServerContextPopulatedEvent`.
3. Узкий пересчёт при смене видимого набора методов типа (см. §7, случай 3): отпечаток
видимого набора при перерегистрации источника методов → перезапись только тех
документов, чьи записи ссылаются на изменённый тип. Для «Переименовать» — обязателен.
4. `DefinitionProvider` — переход к определению как частный случай (читает
`PlatformMemberSymbol.descriptor.sourceSymbol`).
5. Включение настройкой + замер скорости на крупном проекте.

### Открытый вопрос (решить до реализации)

Объём первого шага для «Найти ссылки» (НЕ для «Переименовать» — там пересчёт обязателен):
- (а) «Найти ссылки» по возможности, без пересчёта по смене видимого набора (проще,
возможен пропуск места вызова до правки файла), или
- (б) сразу полный пересчёт и для «Найти ссылки» (вернее, дороже)?

Решено по сути: «потерпеть устаревание, библиотеки редко» снято — ссылаются в первую
очередь на типы кода самого проекта, который правят постоянно, поэтому пересчёт при
смене видимого набора обязателен хотя бы для «Переименовать».

## Карта задействованных файлов

| Файл | Роль |
|------|------|
| `references/PlatformMemberReferenceFinder.java:59-77` | @Order(200), заворачивает метод в `PlatformMemberSymbol` |
| `types/symbol/PlatformMemberSymbol.java` | служебный символ, не `SourceDefinedSymbol`, несёт `MemberDescriptor` |
| `providers/DefinitionProvider.java:112-116` | отсев `isSourceDefinedSymbolReference` → не пускает переход |
| `providers/HoverProvider.java` | строит подсказку по классу символа (работает) |
| `types/model/MemberDescriptor.java:66` | поле `sourceSymbol` + `withSourceSymbol`/`getSourceSymbol` |
| `types/oscript/OScriptModuleMembersProvider.java:261-294` | `.withSourceSymbol(method)` — уже подкладывает исходный символ |
| `references/ReferenceIndexFiller.java` | обходчик кода; разбирает по тексту `Новый Класс()` / `ОбщийМодуль("X")` |
| `references/ReferenceIndex.java:76-102,184-240` | `getReferencesTo` (поиск по ключу), `addMethodCall` |
| `references/ReferenceIndexReferenceFinder.java` | @Order(40), поиск в индексе |
| `providers/ReferencesProvider.java:61-81` | «Найти ссылки» = найти символ + `getReferencesTo` |
| `types/TypeService.java:445-477` | `memberAt`/`membersAt`, `definingSymbol`, `definingUri` |
| `context/events/ServerContextPopulatedEvent.java` | момент «всё загружено» для полного обхода |
| `types/registry/GlobalScopeProvider` | `indexModuleType(uri, ref)` — обратный указатель путь→тип |
Loading