Skip to content

Diagnostics structure, files contents and purpose

This article contains rules of diagnostics usage, creation and information about content templates.

Diagnostics structure

Diagnostics consists of a set of files, which are described in detail in the sections below.
The required set of files as part of the diagnostics at the time of this writing and the rules for their naming

  • Diagnostic implementation class. The file name is formed according to the principle % Diagnostic Key% + Diagnosctic.java
  • Diagnostics test class. The file name is generated according to the principle %DiagnosticKey% + DiagnoscticTest.java
  • Diagnostic resource file in Russian. The file name is formed according to the principle %DiagnosticKey% + Diagnosctic_en.properties
  • Diagnostic resource file in English. The file name is formed according to the principle %DiagnosticKey% + Diagnosctic_en.properties
  • Resource file (fixture) test. The file name is formed according to the principle %DiagnosticKey% + Diagnosctic.bsl
  • Diagnostic description file in Russian. The file name is formed according to the principle %DiagnosticKey% + .md
  • Diagnostic resource file in English. The file name is formed according to the principle %DiagnosticKey% + .md

Note:
To create necessary files in right places, should run command gradlew newDiagnostic --key="KeyDiagnostic", where KeyDiagnostic should be replaced with your own diagnostics key. Details in help gradlew -q help --task newDiagnostic.

Diagnostics implementation class

Diagnostics is implemented by adding a java class to the com.github._1c_syntax.bsl.languageserver.diagnostics package in the src/main/java directory.

In the body of the file, you need to specify the package to which the class and the import block are added (when using ide, the import list is updated automatically). It is necessary to ensure that only is imported that is necessary for implementation, everything unused should be removed (if settings are correct, then ide will do everything automatically).

Each diagnostic must have a @DiagnosticMetadata, class annotation containing diagnostic metadata. The actual content can always be obtained by examining the file.

At the time of this writing, the following properties are available:

  • The type of diagnostics is type and its importance is severity, for each diagnostics it is necessary to define them. In order to choose the correct type and importance of diagnostics, you can refer to article.
  • Time to fix issue minutesToFix (default 0). This value is used when calculating the total technical debt of the project in labor costs to correct all comments (the sum of time to correct for all detected comments). It is worth indicating the time, as realistic as possible, that the developer should spend on fixing.
  • Using the extraMinForComplexity parameter, you can dynamically increase the time to correct a comment for diagnostics that take into account several places that violate the rule, for example, when calculating the complexity of a method.
  • A set of diagnostics tags tag that indicate the group to which it belongs. Read more about tags in the article.
  • Applicability limit scope (by default ALL, i.e. no limit). BSL LS supports multiple languages (oscript and bsl) and diagnostics can be applied to one specific language or to all at once.
  • Default diagnostic active activatedByDefault (default True). When developing experimental, controversial, or not applicable in most projects, it is worth turning off diagnostics by default, the activation will be performed by the end user of the solution.
  • Compatibility mode compatibilityMode, by which diagnostics are filtered when using metadata. The default is UNDEFINED.
  • List of module types modules for the ability to limit the area analyzed by diagnostics
  • Sign of the ability to set issues on the entire project canLocateOnProject. Used for diagnostics not related to the source code module. At the moment, the option is accepted only by SonarQube, other tools ignore it. The last two can be omitted.

Annotation example

@DiagnosticMetadata(
  type = DiagnosticType.CODE_SMELL, 
  severity = DiagnosticSeverity.MINOR, 
  minutesToFix = 1,                    
  activatedByDefault = false,   
  scope = DiagnosticScope.BSL,
  compatibilityMode = DiagnosticCompatibilityMode.COMPATIBILITY_MODE_8_3_3, 
  tags = {
    DiagnosticTag.STANDARD             
  },
  modules = {
    ModuleType.CommonModule       
  },
  canLocateOnProject = false,          
  extraMinForComplexity = 1  // For each additional note position (`DiagnosticRelatedInformation`) one minute will be added
)

Class should implement the interface BSLDiagnostic. If diagnostic bases on AST, that class should extends at one of classes, that implement BSLDiagnostic below:

  • for simple diagnostics (module context checking) it is worth using inheritance AbstractVisitor with the implementation of a single check method
  • if you need to analyze a visit to a node / sequence of nodes, use the listener strategy, you need to inherit the class from AbstractListenerDiagnostic
  • in other cases, you need to use the strategy visitor and
  • AbstractVisitorDiagnostic for diagnostics of 1C code
  • AbstractSDBLVisitorDiagnostic for diagnostics of 1C query

Examples

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

Diagnostic may provide so-called quick fixes. In order to provide quick fixes the diagnostic class must implement QuickFixProvider interface. See this article on adding a quick fix to diagnostic.

Examples

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

After the declaration of the class, a block with their parameters is located for parameterizable diagnostics. For details on the diagnostic parameters, see the article.

Below are the differences in the implementation of diagnostic classes.

Diagnostics class, implements BSLDiagnostic interface

In the class, you need to define a private field diagnosticStorage of type DiagnosticStorage to store detected diagnostics, and a private property info of type DiagnosticInfo, for access to diagnostic data.

 private DiagnosticStorage diagnosticStorage = new DiagnosticStorage(this);
private final DiagnosticInfo info;

In the class, you need to implement:

  • method getDiagnostics accepting the context of the file being analyzed and returning a list of detected diagnostics List<Diagnostic>
  • getter getInfo
  • setter setInfo

Method structure getDiagnostics

  @Override
  public List<Diagnostic> getDiagnostics(DocumentContext documentContext) {
    // Clearing diagnostics storage
    diagnosticStorage.clearDiagnostics();

    documentContext.getComments()  // Getting the collection of tokens, here comments
      .parallelStream()
      .filter((Token t) ->         // Search for "necessary" - those that the diagnostics is aimed at detecting
        !goodCommentPattern.matcher(t.getText()).matches())
      .sequential()
      .forEach((Token t) ->        // Adding errors, here for each token a separate error
        diagnosticStorage.addDiagnostic(t));

    // Return of found
    return diagnosticStorage.getDiagnostics();
  }

Diagnostics class, inherits from AbstractDiagnostic

For simple diagnostics, you should inherit your class from the AbstractDiagnostic class. In the diagnostic class, you need to implement the check method. The method should analyze the context of the document and, if noted, add diagnostics to diagnosticStorage.

Example:

  @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));
  }

Diagnostics class, inherits from AbstractVisitorDiagnostic

In the diagnostic class, it is necessary to implement the methods of all necessary AST visitors, in accordance with the language grammar described in the BSLParser project. A complete list of existing visitor methods can be found in the BSLParserBaseVisitor class. Please note: for simplicity, generalized visitors have been created, for example, instead of two visitFunction for a function and visitProcedure for a procedure, you can use visitSub.

As a parameter, an AST node of the corresponding type is passed to each method of the visitor. In the body of the method, it is necessary to analyze the node and / or its child nodes and decide if there is an error. When a problem is found, it must be added to diagnosticStorage (the field is already defined in the abstract class). An error note can be attached to the passed node or to its child or parent nodes, to the desired block of code.

Method structure

  @Override
  public ParseTree visitModuleVar(BSLParser.ModuleVarContext ctx) {                 // Visitor for module variables
    if(Trees.findAllRuleNodes(ctx, BSLParser.RULE_compilerDirective).size() > 1) {  // Finding child nodes
      diagnosticStorage.addDiagnostic(ctx);                                         // Adding a error to the entire site
    }
    return ctx;
  }

If the diagnostics does not provide analysis of nested nodes, then it must return the passed input node, otherwise it is necessary to call the super-method of the same name.
This rule will save application resources without making a meaningless call.

Examples:

  • Diagnostics for a method or file must immediately return a value, because nested methods/files do not exist
  • Diagnostics for a condition or region block must call the super-method, as they exist and are used (e.g. return super.visitSub(ctx) for methods)

Diagnostics class, inherits from AbstractSDBLVisitorDiagnostic

The diagnostic class implements the necessary AST visitors, according to the grammar of the query language (see BSLParser). The complete list of visitor methods is in the SDBLParserBaseVisitor class.

The rest of the rules are identical to AbstractVisitorDiagnostic.

Diagnostics class, inherits from AbstractListenerDiagnostic (Work in Progress)

Diagnostics test class

The tests use the JUnit5 framework and the AssertJ assertion library providing a fluent interface "expectations", like the familiar asserts library for OneScript.

A test is a java class added to the com.github._1c_syntax.bsl.languageserver.diagnostics package in the src/test/java directory.

In the file, you need to specify the package in which the class and the import block (similar to the diagnostic implementation class) are added, then you need to create a class of the same name to the file, inherited from the AbstractDiagnosticTest class for the created diagnostic class.

Test class example

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);
    }
}

To add a new test to the created class, you need to add a method with the @Test annotation.

The test class must contain methods for testing.

  • diagnostic test itself
  • test of configuration method for parameterized diagnostics
  • test "quick fixes" if available

Diagnostics test

Simplified, the diagnostic test contains the steps

  • getting a list of diagnostics
  • checking the number of items found
  • checking the location of detected items

The first step is to get the list of notes by calling the getDiagnostics() method (implemented in the AbstractDiagnosticTest class). Calling this method will parse the diagnostics resource file and return a list of remarks in it. The next step is to use the hasSize() statement to make sure that the number of diagnostics is fixed as much as allowed in the fixtures. After that, you need to make sure that the diagnostics are detected correctly. To do this, you need to compare the diagnostic area obtained by the getRange() method with the expected area (you should use the RangeHelper class to simplify the formation of control values). If the text of the error is templated, then it is necessary to check it in the test by getting the text of the error message using the getMessage() method of diagnostics.

Test method example

    @Test
    void test() {
      List<Diagnostic> diagnostics = getDiagnostics();   // getting a list of diagnostics

      assertThat(diagnostics).hasSize(2);                // checking the number of errors found

      // verification of special cases
      assertThat(diagnostics)
        .anyMatch(diagnostic -> diagnostic.getRange().equals(Ranges.create(27, 4, 27, 29)))
        .anyMatch(diagnostic -> diagnostic.getRange().equals(Ranges.create(40, 4, 40, 29)));
    }

To reduce the amount of test code, you can use the util.Assertions.assertThat helper, then the example above will look like this:

    @Test
    void test() {
      List<Diagnostic> diagnostics = getDiagnostics();   // getting a list of diagnostics

      assertThat(diagnostics).hasSize(2);                // checking the number of errors found

      // verification of special cases
      assertThat(diagnostics, true)
        .hasRange(27, 4, 27, 29)
        .hasRange(40, 4, 40, 29);
    }

Test of configuration method for parameterized diagnostics

Tests for the configuration method should cover all possible settings and their combinations. The test has almost the same structure as the diagnostic test. Before setting new values ​​for diagnostic parameters, you must get the default diagnostic settings using the getDefaultDiagnosticConfiguration() method using the information of the current diagnostic object diagnosticInstance.getInfo(). The result is a map in which the put method needs to change the values ​​of the required parameters. To apply the changed settings, you need to call the configure() method of the current diagnostic object diagnosticInstance.

Test method example

    @Test
    void testConfigure() {
        // getting default diagnostic settings
        Map<String, Object> configuration = diagnosticInstance.getInfo().getDefaultDiagnosticConfiguration();

        configuration.put("templateParem", "newValue");     // setting "templateParem" to "newValue"
        diagnosticInstance.configure(configuration);        // applying settings

        List<Diagnostic> diagnostics = getDiagnostics();    // getting a list of diagnostics

        assertThat(diagnostics).hasSize(2);                 // checking the number of detected

        // special case check
        assertThat(diagnostics, true)
          .hasRange(27, 4, 27, 29)
          .hasRange(40, 4, 40, 29);
    }

Quick fixes test (Work in progress)

Diagnostics resources

BSL LS supports two languages ​​in diagnostics: Russian and English, so the diagnostics includes two resource files located in the src/main/resources directory in the com.github._1c_syntax.bsl.languageserver.diagnostics package, one for each language. The file structure is the same: it is a text file in UTF-8 encoding, each line of which contains a "Key=Value" pair.

Required parameters used when adding diagnostics using the diagnosticStorage.addDiagnostic method

  • diagnosticMessage - diagnostic message. Value supports parameterization (see String.format)
  • diagnosticName - Diagnostic name, human-readable

For quick fixes, the quickFixMessage parameter is used, which contains a description of the fix action.

Diagnostics test resources

The fixtures are the contents of the test resource file located in the src/test/resources directory in the diagnostics package. The file must contain the necessary code examples in 1C language (or oscript language).

It is necessary to add both erroneous and correct code, marking the places of errors with comments. It is best if the test cases are real, from practice, and not synthetic, invented for diagnostics.

Description

The diagnostic description is created in the Markdown format in two versions - for Russian and English. The files are located in the docs/diagnostics directory for Russian, for English in docs/en/diagnostics. The file has the structure

  • Header equal to the value of diagnosticName from the corresponding language's diagnostic resource file
  • A block with a description of the diagnostics, indicating "why it is so bad"
  • List of exceptions that diagnostics do not detect
  • Examples of good and bad code
  • The diagnostic algorithm, if it is not obvious
  • If diagnostics is an implementation of the standard, then links to sources (for example, to ITS).