В данной статье содержится информация о правилах использования, создания и шаблонах содержимого диагностики.
- Структура диагностики, назначение и содержимое файлов
Диагностика состоит из набора файлов, подробное описание которых приведено в разделах ниже.
Необходимый набор файлов в составе диагностики на момент написания статьи и правила их именования
- Класс реализации диагностики. Имя файла образуется по принципу
%КлючДиагностики%
+Diagnosctic.java
- Класс теста диагностики. Имя файла образуется по принципу
%КлючДиагностики%
+DiagnoscticTest.java
- Файл ресурса диагностики на русском языке. Имя файла образуется по принципу
%КлючДиагностики%
+Diagnosctic_ru.properties
- Файл ресурса диагностики на английском языке. Имя файла образуется по принципу
%КлючДиагностики%
+Diagnosctic_en.properties
- Файл ресурса (фикстура) теста. Имя файла образуется по принципу
%КлючДиагностики%
+Diagnosctic.bsl
- Файл описания диагностики на русском языке. Имя файла образуется по принципу
%КлючДиагностики%
+.md
- Файл ресурса диагностики на английском языке. Имя файла образуется по принципу
%КлючДиагностики%
+.md
Примечание:
Для создания нужных файлов в нужных местах, необходимо выполнить команду gradlew newDiagnostic --key="KeyDiagnostic"
, вместо KeyDiagnostic
необходимо указть ключ своей диагностики. Подробная информация в справке gradlew -q help --task newDiagnostic
.
Диагностика реализуется посредством добавления java-класса в пакет com.github._1c_syntax.bsl.languageserver.diagnostics
в каталоге src/main/java
.
В теле файла, нужно указать пакет, в который добавлен класс и блок импорта (при использовании ide список импорта обновляется автоматически). Необходимо следить за тем, чтобы импортировались только то, что необходимо для реализации, все неиспользуемое должно быть удалено (если настройки выполнены верно, то ide сделает все автоматически).
Каждый класс диагностики должен иметь аннотацию @DiagnosticMetadata
, содержащую метаданные диагностики. Актуальное содержимое всегда можно получить изучив файл.
На момент написания статьи имеются следующие свойства:
- Тип диагностики
type
и ее важностьseverity
, для каждой диагностики обязательно их определение. Для того, чтобы правильно выбрать тип и важность диагностики, можно обратиться к статье. - Время на исправление замечания
minutesToFix
(по умолчанию 0). Данное значение используется при расчете общего техдолга проекта в трудозатрах на исправление всех замечаний (сумма времени на исправление по всем обнаруженным замечаниям). Стоит указывать время, максимально реалистичное, которое разработчик должен потратить на исправление. - Набор тэгов
tag
диагностики, указывающих группы, к котором она относится. Подробнее о тэга в статье. - Границы применимости
scope
(по умолчаниюALL
, т.е. без ограничения). BSL LS поддерживает несколько языков (oscript и bsl) и диагностики могут применяться как к одному конкретному языку, так и ко всем сразу. - Активность правила по-умолчанию
activatedByDefault
(по умолчаниюИстина
). При разработке экспериментальных, спорных либо не применимых в большинстве проектов, стоит по умолчанию отключать диагностику, активацию выполнит конечный пользователь решения. - Режим совместимости
compatibilityMode
, по которому фильтруются диагностики при использовании метаданных. По умолчаниюUNDEFINED
.
Последние два могут быть опущены.
Пример аннотации
@DiagnosticMetadata(
type = DiagnosticType.CODE_SMELL, // Тип Дефект кода
severity = DiagnosticSeverity.MINOR, // Важность Незначительный
minutesToFix = 1, // Время на исправление 1 минута
activatedByDefault = false, // По умолчанию деактивирована
scope = DiagnosticScope.BSL, // Применяется только для BSL
compatibilityMode = DiagnosticCompatibilityMode.COMPATIBILITY_MODE_8_3_3, // Режим проверки совместимости с 8.3.3
tags = {
DiagnosticTag.STANDARD // Относится к диагностикам нарушения стандарта 1С
}
)
Класс должен реализовывать интерфейс BSLDiagnostic
. Если диагностика основывается на AST дереве, то класс реализации должен быть унаследован от одного из классов ниже, реализующих BSLDiagnostic
:
- для простых диагностик (проверка контекста модуля) стоит использовать наследование
AbstractVisitor
с реализацией единственного методаcheck
- при необходимости анализа посещения узла / последовательности узлов, использовать стратегию
слушателя
нужно наследовать класс отAbstractListenerDiagnostic
- в остальных случаях нужно использовать стратегию
визитера
иAbstractVisitorDiagnostic
для диагностик кода 1СAbstractSDBLVisitorDiagnostic
для диагностик запросов 1С
Примеры
public class TemplateDiagnostic implements BSLDiagnostic
public class TemplateDiagnostic extends AbstractDiagnostic
public class TemplateDiagnostic extends AbstractVisitorDiagnostic
public class TemplateDiagnostic extends AbstractListenerDiagnostic
public class TemplateDiagnostic extends AbstractSDBLVisitorDiagnostic
public class TemplateDiagnostic extends AbstractSDBLListenerDiagnostic
Диагностика может предоставлять т.н. быстрые исправления
, для чего класс диагностики должен реализовывать интерфейс QuickFixProvider
. Подробно о добавлении быстрых исправлений
в диагностику написано статье.
Примеры
public class TemplateDiagnostic implements BSLDiagnostic, QuickFixProvider
public class TemplateDiagnostic extends AbstractDiagnostic implements QuickFixProvider
public class TemplateDiagnostic extends AbstractVisitorDiagnostic implements QuickFixProvider
public class TemplateDiagnostic extends AbstractListenerDiagnostic implements QuickFixProvider
public class TemplateDiagnostic extends AbstractSDBLVisitorDiagnostic implements QuickFixProvider
public class TemplateDiagnostic extends AbstractSDBLListenerDiagnostic implements QuickFixProvider
После объявления класса, для параметризуемых диагностик располагается блок с их параметрами. Подробно о параметрах диагностик написано в статье.
Ниже приведены отличия в реализации классов диагностик.
В классе необходимо определить приватное поле diagnosticStorage
типа DiagnosticStorage
, которое будет хранилищем обнаруженных замечаний, и приватное свойство info
типа DiagnosticInfo
, которое будет предоставлять доступ к данным диагностики.
private DiagnosticStorage diagnosticStorage = new DiagnosticStorage(this);
private final DiagnosticInfo info;
В классе необходимо реализовать:
- метод
getDiagnostics
принимающий контекст анализируемого файла и возвращающий список обнаруженных замечанийList<Diagnostic>
- метод
getInfo
, возвращающий значение свойстваinfo
- метод
setInfo
, для установки значения свойстваinfo
Ниже приведена общая структура метода getDiagnostics
@Override
public List<Diagnostic> getDiagnostics(DocumentContext documentContext) {
// Очистка хранилища диагностик
diagnosticStorage.clearDiagnostics();
documentContext.getComments() // Получение коллекции токенов, в примере комментариев
.parallelStream()
.filter((Token t) -> // Поиск "нужных", т.е. тех, на обнаружение которых направлена диагностика
!goodCommentPattern.matcher(t.getText()).matches())
.sequential()
.forEach((Token t) -> // Добавление замечаний, в примере на каждый токен отдельное замечание
diagnosticStorage.addDiagnostic(t));
// Возврат обнаруженных замечаний
return diagnosticStorage.getDiagnostics();
}
Для простых диагностик стоит наследовать класс своей диагностики от класса AbstractDiagnostic.
В классе диагностики необходимо реализовать метод check
- он должен проанализировать контекст документа и, при наличии замечаний, добавить диагностику в diagnosticStorage
.
Пример:
@Override
protected void check() {
documentContext.getTokensFromDefaultChannel()
.parallelStream()
.filter((Token t) ->
t.getType() == BSLParser.IDENTIFIER &&
t.getText().toUpperCase(Locale.ENGLISH).contains("Ё"))
.forEach(token -> diagnosticStorage.addDiagnostic(token));
}
В классе диагностики необходимо реализовать методы всех соответствующих визитеров AST
, в соответствии грамматикой языка, описанной в проекте BSLParser. Полный список существующих методов-визитеров находится в классе BSLParserBaseVisitor
. Необходимо обратить внимание, что для упрощения добавлены обобщенные
визитеры, например вместо реализации visitFunction
для функции и visitProcedure
для процедуры можно использовать visitSub
, обобщающий работу с методами.
В качестве параметра, в каждый метод визитера передается узел AST соответствующего типа. В теле метода необходимо проанализировать узел и/или его дочерние узлы и принять решение о наличии замечания. При обнаружении проблемы, необходимо добавить замечание в хранилище diagnosticStorage
(поле уже определено в абстрактном классе). Замечания может быть привязано как непосредственно к переданному узлу, так и к его дочерним или родительским узлам, к нужному блоку кода.
Примерная структура метода
@Override
public ParseTree visitModuleVar(BSLParser.ModuleVarContext ctx) { // Визитер для переменных модуля
if(Trees.findAllRuleNodes(ctx, BSLParser.RULE_compilerDirective).size() > 1) { // Поиск нужных дочерних узлов
diagnosticStorage.addDiagnostic(ctx); // Добавление замечания на весь узел
}
return ctx;
}
Если диагностика не предусматривает анализ вложенных блоков, то она должна возвращать переданный входной параметр, в противном случае необходимо вызвать аналогичный super-метод
.
Следует внимательно относиться к этому правилу, т.к. оно позволит сэкономить ресурсы приложения не выполняя бессмысленный вызов.
Примеры:
- Диагностика для метода или файла должна сразу возвращать значение, т.к. вложенных методов / файлов не существует
- Диагностика для блока условия или области должна вызывать
super-метод
, т.к. они существуют и используются (напримерreturn super.visitSub(ctx)
для методов)
В классе диагностики необходимо реализовать методы всех соответствующих визитеров AST
, в соответствии грамматикой языка запросов, описанной в проекте BSLParser. Полный список существующих методов-визитеров находится в классе SDBLParserBaseVisitor
.
Остальные правила использования идентичны AbstractVisitorDiagnostic
.
<В разработке>
При написании тестов используется фреймворк JUnit5, для утверждений - библиотека AssertJ, предоставляющая текучий/fluent-интерфейс "ожиданий", подобно привычной многим библиотеке asserts для OneScript.
Теста реализуется посредством добавления java-класса в пакет com.github._1c_syntax.bsl.languageserver.diagnostics
в каталоге src/test/java
.
В теле файла, нужно указать пакет, в который добавлен класс и блок импорта (аналогично классу реализации диагностики).
В файле необходимо создать одноименный файлу класс, унаследованый от класса AbstractDiagnosticTest
для созданного класса диагностики.
Пример тестового класса
package com.github._1c_syntax.bsl.languageserver.diagnostics;
import org.eclipse.lsp4j.Diagnostic;
import com.github._1c_syntax.bsl.languageserver.utils.Ranges;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class TemplateDiagnosticTest extends AbstractDiagnosticTest<TemplateDiagnostic> {
TemplateDiagnosticTest() {
super(TemplateDiagnostic.class);
}
}
Для добавления нового теста в созданный класс, необходимо добавить процедуру, аннотированную как тест @Test
.
В тестовом классе обязательно должны присутствовать методы для тестирования
- тест диагностики, самой по себе
- тест метода конфигурирования для параметризованных диагностик
- тест "быстрых замен" при их наличии
Упрощенно, тест диагностики состоит из следующих шагов
- получение списка замечаний диагностики
- проверка количества срабатываний
- проверка местоположения срабатываний
Первый шагом необходимо получить список замечаний диагностики вызовом метода getDiagnostics()
(реализован в классе AbstractDiagnosticTest
). При вызове этого метода будет выполнен анализ файла ресурса диагностики и возвращен список замечаний в нем.
Следующим шагом необходимо, с помощью утверждения hasSize()
убедиться, что замечаний зафиксированно столько, сколько допущенно в фикстурах.
После этого, необходимо удостовериться, что замечания обнаружены верно, для чего нужно сравнить область замечания, полученную методом getRange()
, с ожидаемой областью (стоит использовать класс RangeHelper
для упрощения формирования контрольнх значений).
В случае использования шаблонного текста сообщения об ошибке замечания, необходимо в тесте проверить и его, получив текст сообщения об ошибке методом getMessage()
диагностики.
Пример тестового метода
@Test
void test() {
List<Diagnostic> diagnostics = getDiagnostics(); // получение списка замечаний диагностики
assertThat(diagnostics).hasSize(2); // проверка количества обнаруженных замечаний
// проверка частных случаев
assertThat(diagnostics)
.anyMatch(diagnostic -> diagnostic.getRange().equals(Ranges.create(27, 4, 27, 29)))
.anyMatch(diagnostic -> diagnostic.getRange().equals(Ranges.create(40, 4, 40, 29)));
}
Для упрощена написания тестов, сокращения объема кода, можно использовать хелпер util.Assertions.assertThat
и тогда пример выше будет выглядеть следующим образом:
@Test
void test() {
List<Diagnostic> diagnostics = getDiagnostics(); // получение списка замечаний диагностики
assertThat(diagnostics).hasSize(2); // проверка количества обнаруженных замечаний
// проверка частных случаев
assertThat(diagnostics, true)
.hasRange(27, 4, 27, 29)
.hasRange(40, 4, 40, 29);
}
Тесты для метода конфигурирования должны покрывать все возможные варианты настроек и их комбинаций. Тест имеет практически ту же структуру, что и тест диагностики, за исключение установки параметров диагностики перед получением спсика замечаний.
Перед установкой новых значений параметров диагностики, необходимо получить настройки диагностики по умолчанию методом getDefaultDiagnosticConfiguration()
, используя информацию текущего объекта диагностики diagnosticInstance.getInfo()
. Полученный результат представляет собой соответствие, в котором, методом put
, необходимо изменить значения нужных параметров. Применение измененных настроек выполняется методом configure()
текущего объекта диагностики diagnosticInstance
.
Пример тестового метода
@Test
void testConfigure() {
// получение настроек диагностики по умолчанию
Map<String, Object> configuration = diagnosticInstance.getInfo().getDefaultDiagnosticConfiguration();
configuration.put("templateParem", "newValue"); // установка параметру "templateParem" значения "newValue"
diagnosticInstance.configure(configuration); // применение настроек
List<Diagnostic> diagnostics = getDiagnostics(); // получение списка замечаний диагностики
assertThat(diagnostics).hasSize(2); // проверка количества обнаруженных замечаний
// проверка частных случаев
assertThat(diagnostics, true)
.hasRange(27, 4, 27, 29)
.hasRange(40, 4, 40, 29);
}
<В разработке>
BSL LS поддерживает два языка в диагностиках: русский и английский, поэтому в состав диагностики входит два файла ресурсов, располагаемых в каталоге src/main/resources
в пакете com.github._1c_syntax.bsl.languageserver.diagnostics
, по одному для каждого языка. Структура файлов одинакова: это текстовый файл в UTF-8 кодировки, каждая строка которого содержит пару "Ключ=Значение".
Обязательные параметры, используемые при добавлении замечания по диагностике методам diagnosticStorage.addDiagnostic
- diagnosticMessage - Сообщение замечания. Значение поддерживает параметризацию (см
String.format
) - diagnosticName - Название диагностики, человекопонятное
Для быстрых исправлений
применяется параметр quickFixMessage
, содержащий описание действия-исправления.
В качестве фикстур используется содержимое ресурсного файла теста, расположенного в каталоге src/test/resources
в пакете diagnostics
. Файл должен содержать необходимые примеры кода на языке 1С (или oscript).
Необходимо добавлять как ошиочный, так и корректный код, помечая с помощью комментариев места, где диагностика должна зафиксировать замечания, а где нет. Лукчше всего, если тестовые примеры будут реальиными
, из практики, а не синтетическими, придуманными под диагностику
.
Описание диагностики создается в формате Markdown в двух вариантах - для русского и английчского языков. Файлы с описанием располагаются в каталоге docs/diagnostics
для русского языка, для английского в docs/diagnostics
.
Файл в общем случае описания имеет следующую структуру
- Заголовок, равный значению
diagnosticName
из файла ресурса диагностики соответствующего языка - Описание параметров диагностики при их наличии
- Тело с описанием диагностики, указанием "почему так плохо"
- Исключительные ситуации, когда диагностика не детектирует замечание
- Примеры плохого и хорошего кода
- Алгоритм работы диагностики для сложных
- Ссылки на источники, если диагностика является реализацией стандарта (например на ИТС).
Кроме непосредственно создания файлов описания диагностики, необходимо обновлять индекс-файлы index.md
для каждого языка, добавляя диагностику в таблицу реализованных.