Skip to content

OwnIR: carry real Roslyn source columns into own-check SARIF #317

Description

@PhysShell

Child of #266. Producer-enablement for the physical anchor OwnAudit's finding-occurrence/v1 uses.

Контракт исправлен. Первая редакция этого issue объявляла 23 записи корпуса «parser-span exclusions» и фиксировала ledger 357/380. Это было построено на неверном чтении check_facts: под-ветка OWN025 была принята за всю flow-local ветку. Разбор ниже — по фактическому коду main@0ded835.

Что делаем

Сегодня Roslyn-путь сворачивает позицию до одной строки:

Roslyn syntax location -> OwnIR fact: line only -> Finding: line only -> SARIF: startLine only

Program.cs:543LineOf(node) => node.GetLocation().GetLineSpan().StartLinePosition.Line + 1. Символ .Character отбрасывается прямо там.

Нужен путь:

node.GetLocation() -> OwnIR optional column -> Finding.column: int | None -> SARIF region.startColumn

Координата принадлежит фактам, а не рендереру: OwnIR — шов между frontend и единственным checker'ом. Producer лишь сохраняет наблюдаемую координату; provenance-логика остаётся в OwnAudit.

Хелпер уже есть — FixSpanOf (Program.cs:570) считает start_line/start_column из того же GetLineSpan().StartLinePosition, но применяется только в лейне --fix-candidates (строки 785, 806). Форму переиспользуем, второй параллельный хелпер не заводим.

Контракт якоря

Строка не меняется. Колонка приходит из того жеSyntaxNode, который сегодня передаётся в LineOf(node) — не выбираем «более красивый» токен заново. Если у категории нет реального Roslyn-узла, колонка отсутствует. Не подставлять 1.

Приёмка

По записанному STS-корпусу (ownsharp.sarif, 613 результатов; 380 scored после вычета 233 advisory OWN050):

corpus total: 380
producer in-scope: 380
producer passed: 380 / 380
parser-span exclusions in corpus: 0
unexpected failures: 0

Почему in-scope именно 380, включая flow-local OWN001

В check_factsтолько специальная ветка OWN025 (POOL005, полнодлинный view пулового буфера) создаёт Finding(line=d.line) — она сознательно репортит на view-site, а не на acquire. Обычная flow-local ветка, включая OWN001, создаёт Finding(line=_as_int(sub.get("line", 0))).

Комментарий дедупликации (ownlang/ownir.py:2911) фиксирует это прямо:

For a flow-local every such diagnostic remaps to the same acquire line (sub["line"]) above, collapsing to byte-identical findings — keep one. The key includes line, so genuinely distinct leak sites stay distinct.

А sub["line"] не приходит из .own-парсера: _lower_flow кладёт n["line"] флоу-опа в acq_line[name] (ownir.py:2234) и дальше в flow-local handle. check_facts строит core Module непосредственно из фактов — без генерации .own-текста и повторного разбора.

Реальная цепочка этих записей:

Roslyn acquire SyntaxNode -> flow op { line, column } -> flow-local handle { line, column }
-> Finding.column -> SARIF region.startColumn

Никаких изменений Diagnostic, _caret_col или .own parser spans для них не требуется.

Разбивка корпуса

ruleresourcenякорь
OWN001subscription token326sub["line"] (subscription fact)
OWN001disposable field24sub["line"] (field fact)
OWN001disposable23sub["line"] (flow-local acquire fact)
OWN014subscription token7sub["line"] (subscription fact)

Отдельная acceptance-группа — flow-local OWN001 acquire anchors (23 записи) — обязана получить настоящие Roslyn-колонки, а не быть исключённой. Это самая содержательная группа среза: именно на ней проверяется, что колонка дошла через flow-op и handle, а не только через прямые facts. В неё входит Core/Mail.cs:32 — известный подтверждённый true positive (SmtpClient с закомментированным //client.Dispose()).

Гейт падает, если

  • любое из 380 in-scope ожиданий не совпало;
  • acceptance-группа flow-local acquire anchors не получила колонки;
  • появилась новая категория расхождения;
  • срез тронул parser/span-код.

Обязательная передача column

Как минимум через:

  • component resource records;
  • flow acquire facts;
  • flow-local handles;
  • Finding.column;
  • SARIF region.startColumn.

Dedupe и сортировка

column обязан войти в dedupe key и в детерминированный sort key. Комментарий на 2911 прямо говорит, что ключ включает line ради различения leak-сайтов; без column две находки на одной строке, различающиеся только колонкой, снова схлопнутся — и тест «несколько findings на одной строке» станет декоративным.

Обязательные тесты

  • old fact without column -> принимается, старый SARIF остаётся без startColumn
  • fact with column -> Finding.column сохранён, SARIF содержит startColumn
  • column = 0 / отрицательное / bool / string -> OwnIRError
  • misleading diagnostic message -> не влияет на emitted startColumn
  • один и тот же исходник с разным отступом -> колонка следует Roslyn location
  • OWNIR_VERSION остаётся 0
  • extractor fixture с несколькими findings на одной строке — колонка различает позиции
  • flow-local acquire anchor: acquire-факт с line + column; core diagnostic может иметь другой d.line; итоговые Finding.line/column обязаны совпасть именно с acquire-фактом; SARIF содержит ту же пару; ни один parser/Diagnostic файл не изменён
  • Rust own-ir round-trip suite проходит: additive-поля сохраняются во flattened extra по его контракту, менять крейт не обязательно — но «необязательно менять» и «можно не проверять» это разные вещи

Версия и порядок полей

OWNIR_VERSIONне повышаем: новое необязательное поле с безопасным default None версию не двигает (spec/OwnIR.md §2). В Finding поле объявляется последним — после ignore_reasoncolumn: int | None = None, чтобы позиционные конструкторы не получили ещё один шанс молча перепутать аргументы.

Затрагиваются: frontend/roslyn/OwnSharp.Extractor/Program.cs, spec/ownir.schema.json, spec/OwnIR.md, ownlang/ownir.py, tests/test_ownir.py, extractor fixtures/tests.

Non-goals

No occurrence/provenance implementation
No OwnAudit changes
No Diagnostic/_caret_col changes
No .own parser-span work
No relatedLocation columns
No human/MSBuild/GitHub rendering change
No locationless recovery
No lineage

No .own parser-span work остаётся, но означает узкое: он ограничивает diagnostic-site колонки — для находок, чей первичный якорь действительно равен сайту диагностики, прежде всего специальной ветки OWN025. Он не исключает acquire-anchored flow-local OWN001, координата которых приходит из Roslyn-факта. В текущих 380 scored rows таких исключений нет.

Diagnostic._caret_col() не трогаем и не переиспользуем: он достаёт subject из текста сообщения, ищет его в строке и откатывается к первому непробельному символу. Это эвристика рендерера. Настоящая колонка для .own-поверхности должна прийти отдельным срезом из parser/token span.

Human, GitHub-annotation и MSBuild output остаются byte-for-byte прежними. Цель конкретна: SARIF producer evidence для occurrence anchor в OwnAudit.

Refs #266, PhysShell/OwnAudit#58.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions