-
Notifications
You must be signed in to change notification settings - Fork 131
docs(session): анализ навигации и find-references для членов через вывод типов #4197
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
nixel2007
wants to merge
3
commits into
develop
Choose a base branch
from
claude/restore-session-context-68k7x2
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+230
−0
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
d78889c
docs(session): анализ навигации и find-references для членов через вы…
claude bf7b997
docs(session): сурфейс-инвалидация обязательна — референсятся типы со…
nixel2007 667d7c3
docs(session): переписать заметки простым русским, без англицизмов-калек
nixel2007 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
230 changes: 230 additions & 0 deletions
230
.session-notes/goto-definition-and-find-references-for-type-inferred-members.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
| ``` | ||
|
|
||
| `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)` — обратный указатель путь→тип | | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
🧰 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
Source: Linters/SAST tools