diff --git a/.session-notes/goto-definition-and-find-references-for-type-inferred-members.md b/.session-notes/goto-definition-and-find-references-for-type-inferred-members.md new file mode 100644 index 00000000000..abbb3e4f8f2 --- /dev/null +++ b/.session-notes/goto-definition-and-find-references-for-type-inferred-members.md @@ -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) +``` + +`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)` — обратный указатель путь→тип |