Работа с синтаксисом

Дерево синтаксиса представляет собой базовую неизменяемую структуру данных, предоставляемую API компилятора. Эти деревья представляют лексическую и синтаксическую структуру исходного кода. Они служат для двух важных целей:

  • Чтобы позволить средствам, таким как интегрированная среда разработки, надстройки, средства анализа кода и рефакторинг, просматривать и обрабатывать синтаксическую структуру исходного кода в проекте пользователя.
  • Чтобы позволить средствам, таким как рефакторинг и интегрированная среда разработки, создавать, изменять и переупорядочивать исходный код естественным образом без прямых изменений текста. Создавая деревья и управляя ими, средства легко могут создавать и переупорядочивать исходный код.

Деревья синтаксиса

Деревья синтаксиса образуют первичную структуру, используемую для компиляции, анализа кода, привязки, рефакторинга, функций интегрированной среды разработки и создания кода. Любая часть исходного кода становится понятной только после того, как будет идентифицирована и классифицирована по одному из многих известных структурных элементов языка.

Примечание.

RoslynQuoter — это средство с открытым исходным кодом, которое показывает вызовы API фабрики синтаксиса, используемые для создания дерева синтаксиса программы. Чтобы попробовать его в прямом эфире, см. статью http://roslynquoter.azurewebsites.net.

Деревья синтаксиса имеют три ключевых атрибута.

  • Они хранят всю информацию об источнике во всей ее полной точности. Полная точность означает, что дерево синтаксиса содержит всю информацию, отраженную в исходном тексте, каждую грамматическую конструкцию, каждую лексему, а также все промежуточные элементы, включая пробелы, комментарии и директивы препроцессора. Например, каждый упомянутый в исходном коде литерал представлен точно так, как был указан. Деревья синтаксиса также перехватывают ошибки в исходном коде, если программа является неполной или неправильно сформированной из-за пропущенных или отсутствующих токенов.
  • С их помощью можно создать точный текст, из которого они были сформированы. От любого синтаксического узла можно получить текстовое представление поддерева, корнем которого он является. Такая возможность означает, что деревья синтаксиса можно использовать для создания и изменения текста исходного кода. Создавая дерево, вы косвенно создаете эквивалентный текст, а получая новое дерево на основе изменений существующего, вы фактически редактируете текст.
  • Они являются неизменяемыми и потокобезопасными. После получения дерево представляет собой моментальный снимок текущего состояния кода и никогда не меняется. Это позволяет нескольким пользователям одновременно взаимодействовать с одним деревом синтаксиса в разных потоках без блокировки или дублирования. Так как деревья являются неизменяемыми, то есть напрямую в них невозможно внести никакие изменения, фабричные методы помогают создавать и изменять деревья синтаксиса путем создания их дополнительных снимков. Деревья эффективно используют базовые узлы повторно, поэтому новую версию можно перестроить быстро и с небольшими затратами памяти.

Дерево синтаксиса фактически является древовидной структурой данных, где нетерминальные структурные элементы являются родительскими для других элементов. Каждое дерево синтаксиса состоит из узлов, токенов и дополнительной синтаксической информации (trivia).

Синтаксические узлы

Синтаксические узлы являются одним из основных элементов деревьев синтаксиса. Они представляют такие синтаксические конструкции, как объявления, операторы, предложения и выражения. Каждая категория синтаксических узлов представлена отдельным классом, производным от Microsoft.CodeAnalysis.SyntaxNode. Набор классов узлов не является расширяемым.

Все синтаксические узлы являются нетерминальными узлами в дереве синтаксиса, значит, всегда имеют дочерние элементы в виде других узлов и токенов. Как дочерний элемент другого узла каждый узел имеет родительский узел, к которому можно обратиться с помощью свойства SyntaxNode.Parent. Так как узлы и деревья являются неизменяемыми, родительский элемент узла никогда не меняется. Корень дерева имеет родительский элемент null.

Каждый узел имеет метод SyntaxNode.ChildNodes(), возвращающий список дочерних узлов в последовательном порядке с учетом их позиции в тексте исходного кода. Этот список не содержит токены. Каждый узел также имеет методы для изучения дочерних объектов, такие как DescendantNodes, DescendantTokens или DescendantTrivia. Они представляют список всех узлов, токенов или дополнительной синтаксической информации, которые присутствуют в поддереве, являющемся корнем этого узла.

Кроме того, каждый подкласс синтаксических узлов предоставляет те же дочерние элементы посредством строго типизированных свойств. Например, класс узлов BinaryExpressionSyntax имеет три дополнительные свойства для отдельных бинарных операторов: Left, OperatorToken и Right. Типом Left и Right является ExpressionSyntax, а типом OperatorTokenSyntaxToken.

Некоторые синтаксические узлы имеют дополнительные дочерние элементы. Например, IfStatementSyntax имеет дополнительный ElseClauseSyntax. Если дочерний элемент не указан, свойство возвращает значение null.

Синтаксические токены

Синтаксические токены являются терминалами грамматики языка, представляющими наименьшие синтаксические фрагменты кода. Они никогда не бывают родителями других узлов или токенов. Синтаксические токены состоят из ключевых слов, идентификаторов, литералов и знаков препинания.

Для повышения эффективности тип SyntaxToken является типом значений среды CLR. Таким образом, в отличие от синтаксических узлов существует всего одна структура для всех видов токенов с набором свойств, значение которых зависит от типа представленного токена.

Например, токен целочисленного литерала представляет числовое значение. Кроме необработанного текста исходного кода, охватываемого токеном, токен литерала имеет свойство Value, сообщающее точное декодированное целочисленное значение. Это свойство имеет тип Object, так как может быть одним из многих типов-примитивов.

Свойство ValueText сообщает те же сведения, что и свойство Value, однако оно всегда имеет тип String. Идентификатор в тексте исходного кода C# может включать escape-символы Юникода, при этом синтаксис самой escape-последовательности не считается частью имени идентификатора. Поэтому хотя охватываемый токеном необработанный текст и включает escape-последовательность, свойство ValueText ее не содержит. Вместо этого оно включает в себя символы Юникода, определяемые этой escape-последовательностью. Например, если текст исходного кода содержит идентификатор, записанный как \u03C0, то свойство ValueText для этого токена возвратит π.

Дополнительная синтаксическая информация

Дополнительная синтаксическая информация (syntax trivia) представляет части текста в исходном коде, которые очень мало влияют на понимание кода, например пробелы, комментарии и директивы препроцессора. Как и синтаксические токены, элементы trivia являются типами значений. Для описания всех видов trivia используется один тип Microsoft.CodeAnalysis.SyntaxTrivia.

Так как элементы trivia не являются частью обычного синтаксиса и могут стоять в любом месте между двумя любыми токенами, они не включаются в дерево синтаксиса в качестве дочернего элемента узла. Хотя, так как они важны при реализации таких функций, как рефакторинг, и обеспечения полного соответствия тексту исходного кода, они входят в состав дерева синтаксиса.

Вы можете обратиться к trivia, просмотрев коллекции SyntaxToken.LeadingTrivia или SyntaxToken.TrailingTrivia токена. При синтаксическом анализе текста исходного кода последовательности trivia сопоставляются с токенами. Как правило, токену принадлежит любой элемент trivia, расположенный в той же строке до следующего токена. Любой элемент trivia после этой строки сопоставляется со следующим токеном. Первый токен в исходном файле получает все начальные элементы trivia, а последняя последовательность trivia в файле присоединяется к токену конца файла, который в противном случае имеет нулевую ширину.

В отличие от синтаксических узлов и токенов элементы syntax trivia не имеют родительских элементов. Однако, так как они являются частью дерева и каждый из них сопоставлен с одним токеном, к соответствующему токену можно получить доступ с помощью свойства SyntaxTrivia.Token.

Диапазоны

Каждому узлу, токену или элементу trivia известно его место в тексте исходного кода и число символов, из которых он состоит. Положение в тексте представлено в виде 32-разрядного целого числа, являющегося отсчитываемым от нуля индексом char. Объект TextSpan обозначает начальное положение и количество символов, представленные в виде целых чисел. Если TextSpan имеет нулевую длину, он обозначает расположение между двумя символами.

У каждого узла есть два свойства TextSpan: Span и FullSpan.

Свойство Span соответствует текстовому диапазону от начала первого токена в поддереве узла до конца последнего токена. Этот диапазон не включает в себя никакие начальные или конечные элементы trivia.

Свойство FullSpan является текстовым диапазоном, включающим в себя обычный диапазон узла, а также диапазон любых начальных или конечных элементов trivia.

Например:

      if (x > 3)
      {
||        // this is bad
          |throw new Exception("Not right.");|  // better exception?||
      }

Узел оператора внутри блока имеет диапазон, обозначаемый отдельными вертикальными чертами (|). Он включает символы throw new Exception("Not right.");. Полный диапазон обозначается двойными вертикальными чертами (||). Он содержит те же символы, что и обычный диапазон, а также символы, связанные с начальными или конечными элементами trivia.

Типы

Каждый узел, токен или элемент trivia имеет свойство SyntaxNode.RawKind типа System.Int32, определяющее конкретный представленный элемент синтаксиса. Это значение можно привести к перечислению определенного языка. Каждый язык, C# или Visual Basic имеет одно перечисление SyntaxKind (Microsoft.CodeAnalysis.CSharp.SyntaxKind и Microsoft.CodeAnalysis.VisualBasic.SyntaxKind соответственно), содержащее все возможные узлы, токены и другие элементы грамматики. Это преобразование может выполняться автоматически с помощью методов расширения CSharpExtensions.Kind или VisualBasicExtensions.Kind.

Свойство RawKind позволяет легко устранить неоднозначность типов синтаксических узлов, которые используют один класс узлов. Для токенов и элементов trivia это свойство является единственным способом отличить один тип элемента от другого.

Например, один класс BinaryExpressionSyntax имеет дочерние элементы Left, OperatorToken и Right. Свойство Kind позволяет определить, имеет ли этот синтаксический узел тип AddExpression, SubtractExpression или MultiplyExpression.

Совет

Рекомендуется проверять типы с помощью методов расширения IsKind (для C#) или IsKind (для VB).

ошибки

Даже если текст исходного кода содержит синтаксические ошибки, предоставляется полное дерево синтаксиса, которое обеспечивает круговой путь до источника. Когда средство синтаксического анализа обнаруживает код, который не соответствует заданному синтаксису языка, для создания дерева синтаксиса используется один из двух способов.

  • Если средство синтаксического анализа ожидает определенный тип токена, но не находит его, оно может добавить отсутствующий токен в дерево синтаксиса в том расположении, где он ожидался. Отсутствующий токен представляет фактический ожидаемый токен, однако он имеет пустой диапазон, а его свойство SyntaxNode.IsMissing возвращает true.

  • Средство синтаксического анализа может пропускать токены, пока не найдет такой, с которого может продолжить анализ. В этом случае пропущенные токены добавляются в виде узла trivia типа SkippedTokensTrivia.