Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions docs/diagnostics/SuspiciousChangeAndValidate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Подозрительное использование &ИзменениеИКонтроль (SuspiciousChangeAndValidate)

<!-- Блоки выше заполняются автоматически, не трогать -->
## Описание диагностики

Метод, помеченный аннотацией `&ИзменениеИКонтроль`, содержит одновременно директивы `#Удаление` / `#КонецУдаления` и `#Вставка` / `#КонецВставки`. Такая комбинация означает полную замену тела метода, что скрывает реальные изменения от ревьюера.

Аннотация `&ИзменениеИКонтроль` предназначена для точечных правок. Для полной замены метода используйте `&Вместо`.

## Примеры

### Неправильно

```bsl
&ИзменениеИКонтроль("МетодИзКонфигурации")
Процедура префМетодИзКонфигурации()

#Удаление
старый код
#КонецУдаления
#Вставка
новый код
#КонецВставки

КонецПроцедуры
```

### Правильно

```bsl
// Точечные изменения
&ИзменениеИКонтроль("МетодИзКонфигурации")
Процедура префМетодИзКонфигурации()
... // оригинальный код

#Удаление
проблемная строка
#КонецУдаления
#Вставка
исправленная строка
#КонецВставки

... // остальной код
КонецПроцедуры

// Или полная замена через &Вместо
&Вместо("МетодИзКонфигурации")
Процедура префМетодИзКонфигурации()
новый код
КонецПроцедуры
```
51 changes: 51 additions & 0 deletions docs/en/diagnostics/SuspiciousChangeAndValidate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Suspicious use of &ChangeAndValidate (SuspiciousChangeAndValidate)

<!-- Блоки выше заполняются автоматически, не трогать -->
## Diagnostic description

A method annotated with `&ChangeAndValidate` contains both `#Delete` / `#EndDelete` and `#Insert` / `#EndInsert` directives, indicating full method body replacement that hides changes from reviewers.

`&ChangeAndValidate` is designed for targeted modifications. For full replacement, use `&Instead`.

## Examples

### Incorrect

```bsl
&ChangeAndValidate("ConfigurationMethod")
Procedure prefConfigurationMethod()

#Delete
old code
#EndDelete
#Insert
new code
#EndInsert

EndProcedure
```

### Correct

```bsl
// Targeted changes
&ChangeAndValidate("ConfigurationMethod")
Procedure prefConfigurationMethod()
... // original code

#Delete
problematic line
#EndDelete
#Insert
fixed line
#EndInsert

... // rest of code
EndProcedure

// Or full replacement via &Instead
&Instead("ConfigurationMethod")
Procedure prefConfigurationMethod()
new code
EndProcedure
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/*
* This file is a part of BSL Language Server.
*
* Copyright (c) 2018-2026
* Alexey Sosnoviy <labotamy@gmail.com>, Nikita Fedkin <nixel2007@gmail.com> and contributors
*
* SPDX-License-Identifier: LGPL-3.0-or-later
*
* BSL Language Server is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3.0 of the License, or (at your option) any later version.
*
* BSL Language Server is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with BSL Language Server.
*/
package com.github._1c_syntax.bsl.languageserver.diagnostics;

import com.github._1c_syntax.bsl.languageserver.context.symbol.MethodSymbol;
import com.github._1c_syntax.bsl.languageserver.context.symbol.annotations.Annotation;
import com.github._1c_syntax.bsl.languageserver.context.symbol.annotations.AnnotationKind;
import com.github._1c_syntax.bsl.languageserver.diagnostics.metadata.DiagnosticMetadata;
import com.github._1c_syntax.bsl.languageserver.diagnostics.metadata.DiagnosticScope;
import com.github._1c_syntax.bsl.languageserver.diagnostics.metadata.DiagnosticSeverity;
import com.github._1c_syntax.bsl.languageserver.diagnostics.metadata.DiagnosticTag;
import com.github._1c_syntax.bsl.languageserver.diagnostics.metadata.DiagnosticType;
import com.github._1c_syntax.bsl.languageserver.utils.Ranges;
import com.github._1c_syntax.bsl.parser.BSLLexer;
import org.antlr.v4.runtime.Token;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;

import java.util.List;

/**
* Метод с аннотацией {@code &ИзменениеИКонтроль}, в котором директивы
* {@code #Удаление} и {@code #Вставка} полностью заменяют тело метода.
* <p>
* Полная замена — когда оригинального кода не остаётся ни до, ни между,
* ни после блоков удаления/вставки. Для такого сценария следует
* использовать аннотацию {@code &Вместо}.
* <p>
* Проверяет наличие обеих пар маркеров
* ({@code #Удаление}+{@code #КонецУдаления},
* {@code #Вставка}+{@code #КонецВставки}), их положение у границ метода
* и отсутствие кода между блоками удаления и вставки.
*
* @see <a href="https://its.1c.ru/db/v8std/content/455/hdoc">Стандарт 455</a>
*/
@DiagnosticMetadata(
type = DiagnosticType.CODE_SMELL,
severity = DiagnosticSeverity.MAJOR,
scope = DiagnosticScope.BSL,
minutesToFix = 5,
tags = {
DiagnosticTag.BADPRACTICE,
DiagnosticTag.SUSPICIOUS
}
)
public class SuspiciousChangeAndValidateDiagnostic extends AbstractDiagnostic {

private static final int PROXIMITY_LINES = 3;

@Override
public void check() {
var tokens = documentContext.getTokens();

documentContext.getSymbolTree().getMethods()
.stream()
.filter(method -> method.getAnnotations().stream()
.map(Annotation::getKind)
.anyMatch(kind -> kind == AnnotationKind.CHANGEANDVALIDATE))
.filter(method -> isFullReplacement(method, tokens))
.forEach(method ->
diagnosticStorage.addDiagnostic(method.getSubNameRange(),
info.getMessage(method.getName())));
}

/**
* Полная замена: обе пары маркеров присутствуют,
* {@code #Удаление} у начала метода, {@code #КонецВставки} у конца,
* и между {@code #КонецУдаления} и {@code #Вставка} нет кода.
*/
private static boolean isFullReplacement(MethodSymbol method, List<Token> tokens) {
var range = method.getRange();
int methodStart = range.getStart().getLine();
int methodEnd = range.getEnd().getLine();

var methodTokens = tokensInRange(tokens, range);

Comment on lines +84 to +95

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Prove that the blocks cover the method body.

The current condition checks only the first deletion line and the last insertion-end line. It still reports a method that keeps original code between those blocks. That is a targeted change, not a full replacement. Match the paired intervals and verify that no original code remains outside them.

Also applies to: 102-109

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/github/_1c_syntax/bsl/languageserver/diagnostics/SuspiciousChangeAndValidateDiagnostic.java`
around lines 82 - 96, Update isFullReplacement to verify that the paired
deletion and insertion intervals collectively cover the entire method body,
rather than relying only on the first `#Удаление` and final `#КонецВставки` markers.
Match each block’s start/end markers and ensure no original tokens or lines
remain outside the covered intervals before returning true.

// Требуем обе пары маркеров
if (!hasPair(methodTokens, BSLLexer.PREPROC_DELETE, BSLLexer.PREPROC_ENDDELETE)
|| !hasPair(methodTokens, BSLLexer.PREPROC_INSERT, BSLLexer.PREPROC_ENDINSERT)) {
return false;
}

int firstDelete = firstLineOf(methodTokens, BSLLexer.PREPROC_DELETE);
int lastEndInsert = lastLineOf(methodTokens, BSLLexer.PREPROC_ENDINSERT);

// Блоки должны быть у границ метода
if (firstDelete < 0 || lastEndInsert < 0) {
return false;
}
if ((firstDelete - methodStart) > PROXIMITY_LINES) {
return false;
}
if ((methodEnd - lastEndInsert) > PROXIMITY_LINES) {
return false;
}

// Между концом удаления и началом вставки не должно быть кода
int endDelete = lastLineOf(methodTokens, BSLLexer.PREPROC_ENDDELETE);
int startInsert = firstLineOf(methodTokens, BSLLexer.PREPROC_INSERT);
if (endDelete < 0 || startInsert < 0) {
return false;
}

return !hasCodeBetween(tokens, endDelete, startInsert);
}

private static boolean hasPair(List<Token> tokens, int startType, int endType) {
return containsToken(tokens, startType) && containsToken(tokens, endType);
}

private static boolean containsToken(List<Token> tokens, int type) {
return tokens.stream().anyMatch(t -> t.getType() == type);
}

/**
* Есть ли код (default-channel токены) между строками {@code fromLine}
* и {@code toLine} включительно. Игнорируем сами маркеры.
*/
private static boolean hasCodeBetween(List<Token> tokens, int fromLine, int toLine) {
return tokens.stream()
.filter(t -> t.getChannel() == Token.DEFAULT_CHANNEL)
.anyMatch(t -> {
int line = t.getLine() - 1;
return line >= fromLine && line <= toLine
&& t.getType() != BSLLexer.PREPROC_ENDDELETE
&& t.getType() != BSLLexer.PREPROC_INSERT;
});
}

private static List<Token> tokensInRange(List<Token> tokens, Range range) {
return tokens.stream()
.filter(token -> Ranges.containsPosition(range,
new Position(token.getLine() - 1, 0)))
.toList();
}

private static int firstLineOf(List<Token> tokens, int type) {
return tokens.stream()
.filter(t -> t.getType() == type)
.mapToInt(t -> t.getLine() - 1)
.min().orElse(-1);
}

private static int lastLineOf(List<Token> tokens, int type) {
return tokens.stream()
.filter(t -> t.getType() == type)
.mapToInt(t -> t.getLine() - 1)
.max().orElse(-1);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
diagnosticMessage=Method "%s" uses &ChangeAndValidate for full code replacement: both #Delete and #Insert detected. Use &Instead
diagnosticName=Suspicious use of &ChangeAndValidate
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
diagnosticMessage=Метод "%s" использует &ИзменениеИКонтроль для полной замены кода: обнаружены #Удаление и #Вставка. Используйте &Вместо
diagnosticName=Подозрительное использование &ИзменениеИКонтроль
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* This file is a part of BSL Language Server.
*
* Copyright (c) 2018-2026
* Alexey Sosnoviy <labotamy@gmail.com>, Nikita Fedkin <nixel2007@gmail.com> and contributors
*
* SPDX-License-Identifier: LGPL-3.0-or-later
*
* BSL Language Server is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3.0 of the License, or (at your option) any later version.
*
* BSL Language Server is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with BSL Language Server.
*/
package com.github._1c_syntax.bsl.languageserver.diagnostics;

import org.eclipse.lsp4j.Diagnostic;
import org.junit.jupiter.api.Test;

import java.util.List;

import static com.github._1c_syntax.bsl.languageserver.util.Assertions.assertThat;

class SuspiciousChangeAndValidateDiagnosticTest
extends AbstractDiagnosticTest<SuspiciousChangeAndValidateDiagnostic> {

SuspiciousChangeAndValidateDiagnosticTest() {
super(SuspiciousChangeAndValidateDiagnostic.class);
}

@Test
void detectsFullReplacementWithDeleteAndInsert() {
List<Diagnostic> diagnostics = getDiagnostics();

// СИзменением: #Удаление в начале + #КонецВставки в конце, кода между
// блоками нет → срабатывает (1)
// БезИзменения: нет &ИзменениеИКонтроль → не срабатывает
// ФункцияСИзменением: оригинальный код до #Удаления → не срабатывает
// ПустаяПроцедура: нет директив → не срабатывает
// НеполнаяПара: нет #КонецУдаления → не срабатывает
// СКодомМеждуБлоками: оригинальный код между #КонецУдаления и
// #Вставка → не срабатывает
assertThat(diagnostics).hasSize(1);
assertThat(diagnostics, true)
.hasRange(1, 10, 1, 21); // СИзменением
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
&ИзменениеИКонтроль
Процедура СИзменением()

#Удаление
старый код
#КонецУдаления
#Вставка
// какой-то код
#КонецВставки

КонецПроцедуры

Процедура БезИзменения()

#Вставка
// какой-то код
#КонецВставки

КонецПроцедуры

&ИзменениеИКонтроль
Функция ФункцияСИзменением()

// оригинальный код

#Удаление
удаляемый блок
#КонецУдаления
#Вставка
// код
#КонецВставки

КонецФункции

Процедура ПустаяПроцедура()
// нет вставки
КонецПроцедуры

&ИзменениеИКонтроль
Процедура НеполнаяПара()

#Удаление
старый код
#Вставка
// код
#КонецВставки

КонецПроцедуры

&ИзменениеИКонтроль
Процедура СКодомМеждуБлоками()

#Удаление
старый код
#КонецУдаления

ОбработчикМеждуБлоками();

#Вставка
// код
#КонецВставки

КонецПроцедуры
Loading