Перейти к содержанию

Структура диагностики, назначение и содержимое файлов

В данной статье содержится информация о правилах использования, создания и шаблонах содержимого диагностики.

Состав диагностики

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

  • Класс реализации диагностики. Имя файла образуется по принципу %КлючДиагностики% + 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). Данное значение используется при расчете общего техдолга проекта в трудозатрах на исправление всех замечаний (сумма времени на исправление по всем обнаруженным замечаниям). Стоит указывать время, максимально реалистичное, которое разработчик должен потратить на исправление.
  • С помощью параметра extraMinForComplexity можно динамически увеличивать время на исправление замечания для диагностик, в которых учитывается несколько нарушающих правило мест, например при расчете сложности метода.
  • Набор тэгов tag диагностики, указывающих группы, к котором она относится. Подробнее о тэга в статье.
  • Границы применимости scope (по умолчанию ALL, т.е. без ограничения). BSL LS поддерживает несколько языков (oscript и bsl) и диагностики могут применяться как к одному конкретному языку, так и ко всем сразу.
  • Активность правила по-умолчанию activatedByDefault (по умолчанию Истина). При разработке экспериментальных, спорных либо не применимых в большинстве проектов, стоит по умолчанию отключать диагностику, активацию выполнит конечный пользователь решения.
  • Режим совместимости compatibilityMode, по которому фильтруются диагностики при использовании метаданных. По умолчанию UNDEFINED.
  • Список типов модулей modules для возможности ограничить анализируемую диагностикой область
  • Признак возможности установить замечания на весь проект canLocateOnProject. Используется для диагностик не связанных с модулем исходного кода. На данный момент опция воспринимается только SonarQube, остальные инструменты игнорируют. Последние два могут быть опущены.

Пример аннотации

@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С
  },
  modules = {
    ModuleType.CommonModule            // Анализируются только общие модули
  },
  canLocateOnProject = false,          // Замечание будет размещено только в привязке к модулю
  extraMinForComplexity = 1            // За каждую дополнительную позицию замечания (`DiagnosticRelatedInformation`) будет добавлено по одной минуте
)

Класс должен реализовывать интерфейс 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

После объявления класса, для параметризуемых диагностик располагается блок с их параметрами. Подробно о параметрах диагностик написано в статье.

Ниже приведены отличия в реализации классов диагностик.

Класс диагностики, реализующий интерфейс BSLDiagnostic

В классе необходимо определить приватное поле 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

Для простых диагностик стоит наследовать класс своей диагностики от класса 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));
  }

Класс диагностики, унаследованный от AbstractVisitorDiagnostic

В классе диагностики необходимо реализовать методы всех соответствующих визитеров 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) для методов)

Класс диагностики, унаследованный от AbstractSDBLVisitorDiagnostic

В классе диагностики необходимо реализовать методы всех соответствующих визитеров AST, в соответствии грамматикой языка запросов, описанной в проекте BSLParser. Полный список существующих методов-визитеров находится в классе SDBLParserBaseVisitor.

Остальные правила использования идентичны AbstractVisitorDiagnostic.

Класс диагностики, унаследованный от AbstractListenerDiagnostic (В РАЗРАБОТКЕ)

<В разработке>

Класс теста диагностики

При написании тестов используется фреймворк 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 из файла ресурса диагностики соответствующего языка
  • Тело с описанием диагностики, указанием "почему так плохо"
  • Исключительные ситуации, когда диагностика не детектирует замечание
  • Примеры плохого и хорошего кода
  • Алгоритм работы диагностики для сложных
  • Ссылки на источники, если диагностика является реализацией стандарта (например на ИТС).