Skip to content

Commit a895d2f

Browse files
committed
Первичная заготовка правила
1c-syntax#536
1 parent a4ad82b commit a895d2f

File tree

3 files changed

+347
-0
lines changed

3 files changed

+347
-0
lines changed
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
/*
2+
* This file is a part of BSL Language Server.
3+
*
4+
* Copyright (c) 2018-2021
5+
* Alexey Sosnoviy <[email protected]>, Nikita Gryzlov <[email protected]> and contributors
6+
*
7+
* SPDX-License-Identifier: LGPL-3.0-or-later
8+
*
9+
* BSL Language Server is free software; you can redistribute it and/or
10+
* modify it under the terms of the GNU Lesser General Public
11+
* License as published by the Free Software Foundation; either
12+
* version 3.0 of the License, or (at your option) any later version.
13+
*
14+
* BSL Language Server is distributed in the hope that it will be useful,
15+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17+
* Lesser General Public License for more details.
18+
*
19+
* You should have received a copy of the GNU Lesser General Public
20+
* License along with BSL Language Server.
21+
*/
22+
package com.github._1c_syntax.bsl.languageserver.diagnostics;
23+
24+
import com.github._1c_syntax.bsl.languageserver.diagnostics.metadata.DiagnosticMetadata;
25+
import com.github._1c_syntax.bsl.languageserver.diagnostics.metadata.DiagnosticSeverity;
26+
import com.github._1c_syntax.bsl.languageserver.diagnostics.metadata.DiagnosticTag;
27+
import com.github._1c_syntax.bsl.languageserver.diagnostics.metadata.DiagnosticType;
28+
import com.github._1c_syntax.bsl.languageserver.utils.Ranges;
29+
import com.github._1c_syntax.bsl.languageserver.utils.Trees;
30+
import com.github._1c_syntax.bsl.parser.BSLParser;
31+
import com.github._1c_syntax.bsl.parser.BSLParser.CodeBlockContext;
32+
import com.github._1c_syntax.bsl.parser.BSLParserRuleContext;
33+
import com.github._1c_syntax.bsl.parser.SDBLParser;
34+
import com.github._1c_syntax.bsl.parser.SDBLParser.ParameterContext;
35+
import com.github._1c_syntax.bsl.parser.SDBLParser.QueryPackageContext;
36+
import com.github._1c_syntax.bsl.parser.Tokenizer;
37+
import com.github._1c_syntax.utils.CaseInsensitivePattern;
38+
import org.antlr.v4.runtime.tree.ParseTree;
39+
import org.apache.commons.lang3.tuple.Pair;
40+
41+
import java.util.ArrayList;
42+
import java.util.Collection;
43+
import java.util.Collections;
44+
import java.util.List;
45+
import java.util.Optional;
46+
import java.util.regex.Pattern;
47+
import java.util.stream.Collectors;
48+
49+
@DiagnosticMetadata(
50+
type = DiagnosticType.CODE_SMELL,
51+
severity = DiagnosticSeverity.INFO,
52+
minutesToFix = 1,
53+
tags = {
54+
DiagnosticTag.SUSPICIOUS
55+
}
56+
57+
)
58+
public class MissingQueryParameterDiagnostic extends AbstractVisitorDiagnostic {
59+
60+
private static final Pattern QUERY_PATTERN = CaseInsensitivePattern.compile(
61+
"Запрос|Query");
62+
private static final Pattern QUERY_TEXT_PATTERN = CaseInsensitivePattern.compile(
63+
"Текст|Text");
64+
// private static final Pattern EXECUTE_PATTERN = CaseInsensitivePattern.compile(
65+
// "Выполнить|Execute");
66+
private static final Pattern SET_PARAMETER_PATTERN = CaseInsensitivePattern.compile(
67+
"УстановитьПараметр|SetParameter");
68+
public static final int SET_PARAMETER_PARAMS_COUNT = 2;
69+
70+
private Collection<CodeBlockContext> codeBlocks = Collections.emptyList();
71+
72+
@Override
73+
public ParseTree visitFile(BSLParser.FileContext file) {
74+
75+
final var queriesWithParams = documentContext.getQueries().stream()
76+
.map(Tokenizer::getAst)
77+
.map(ctx -> Pair.of(ctx, getParams(ctx)))
78+
.collect(Collectors.toList());
79+
80+
if (!queriesWithParams.isEmpty()) {
81+
codeBlocks = getCodeBlocks();
82+
83+
queriesWithParams.forEach(query -> visitQuery(query.getLeft(), query.getRight()));
84+
85+
codeBlocks.clear();
86+
87+
}
88+
89+
return super.visitFile(file);
90+
}
91+
92+
private Collection<CodeBlockContext> getCodeBlocks() {
93+
final var ast = documentContext.getAst();
94+
var blocks = getSubBlocks(ast);
95+
final var fileCodeBlock = Optional.ofNullable(ast.fileCodeBlock()).map(BSLParser.FileCodeBlockContext::codeBlock);
96+
fileCodeBlock.ifPresent(blocks::add);
97+
final var fileCodeBlockBeforeSub =
98+
Optional.ofNullable(ast.fileCodeBlockBeforeSub())
99+
.map(BSLParser.FileCodeBlockBeforeSubContext::codeBlock);
100+
fileCodeBlockBeforeSub.ifPresent(blocks::add);
101+
return blocks;
102+
}
103+
104+
private void visitQuery(QueryPackageContext queryPackage, List<ParameterContext> params) {
105+
final var codeBlockByQuery = getCodeBlockByQuery(queryPackage);
106+
codeBlockByQuery.ifPresent(block -> checkQueriesInsideBlock(block, queryPackage, params));
107+
}
108+
109+
private static Collection<CodeBlockContext> getSubBlocks(BSLParser.FileContext ast) {
110+
if (ast.subs() == null) {
111+
return new ArrayList<>();
112+
}
113+
return ast.subs().sub().stream().map((BSLParser.SubContext sub) -> {
114+
if (sub.procedure() == null) {
115+
return sub.function().subCodeBlock().codeBlock();
116+
} else {
117+
return sub.procedure().subCodeBlock().codeBlock();
118+
}
119+
}).collect(Collectors.toList());
120+
}
121+
122+
private static List<ParameterContext> getParams(QueryPackageContext queryPackage) {
123+
return Trees.findAllRuleNodes(queryPackage, SDBLParser.RULE_parameter).stream()
124+
.filter(ParameterContext.class::isInstance)
125+
.map(ParameterContext.class::cast)
126+
.collect(Collectors.toList());
127+
}
128+
129+
private Optional<CodeBlockContext> getCodeBlockByQuery(QueryPackageContext key) {
130+
return codeBlocks.stream()
131+
.filter(block -> Ranges.containsRange(Ranges.create(block), Ranges.create(key)))
132+
.findFirst();
133+
}
134+
135+
private void checkQueriesInsideBlock(CodeBlockContext codeBlock, QueryPackageContext queryPackage,
136+
List<ParameterContext> params) {
137+
// todo вычислять единожды сразу с отбором Новый Запрос или ХХХ.Текст
138+
final var assignments = Trees.findAllRuleNodes(codeBlock, BSLParser.RULE_assignment);
139+
140+
final var range = Ranges.create(queryPackage);
141+
assignments.stream()
142+
.filter(BSLParser.AssignmentContext.class::isInstance)
143+
.map(BSLParser.AssignmentContext.class::cast)
144+
.filter(assignment -> Ranges.containsRange(Ranges.create(assignment.expression()), range))
145+
.forEach(assignment -> checkAssignment(codeBlock, queryPackage, params, assignment));
146+
}
147+
148+
private void checkAssignment(CodeBlockContext codeBlock, QueryPackageContext queryPackage,
149+
List<ParameterContext> params, BSLParser.AssignmentContext assignment) {
150+
151+
var callStatements = getCallStatements(codeBlock); // todo вычислять единожды
152+
if (!callStatements.isEmpty()) {
153+
154+
final var queryRange = Ranges.create(queryPackage);
155+
final var queryVarName = computeQueryVarName(assignment);
156+
final var usedParams = callStatements.stream()
157+
.filter(callStatement -> Ranges.containsRange(Ranges.create(assignment.expression()), queryRange))
158+
.map(callStatementContext -> validateSetParameterCallForName(callStatementContext, queryVarName, params))
159+
.flatMap(Optional::stream)
160+
.collect(Collectors.toList());
161+
params.removeAll(usedParams);
162+
}
163+
params.forEach(diagnosticStorage::addDiagnostic);
164+
}
165+
166+
private static List<BSLParser.CallStatementContext> getCallStatements(CodeBlockContext codeBlock) {
167+
return Trees.findAllRuleNodes(codeBlock, BSLParser.RULE_callStatement).stream()
168+
.filter(BSLParser.CallStatementContext.class::isInstance)
169+
.map(BSLParser.CallStatementContext.class::cast)
170+
.filter(MissingQueryParameterDiagnostic::isSetParameterCall)
171+
.collect(Collectors.toList());
172+
}
173+
174+
private static boolean isSetParameterCall(BSLParser.CallStatementContext callStatement) {
175+
return Optional.of(callStatement)
176+
.map(BSLParser.CallStatementContext::accessCall)
177+
.map(BSLParser.AccessCallContext::methodCall)
178+
.map(BSLParser.MethodCallContext::methodName)
179+
.filter(ctx -> SET_PARAMETER_PATTERN.matcher(ctx.getText()).matches())
180+
.isPresent();
181+
}
182+
183+
private static String computeQueryVarName(BSLParser.AssignmentContext assignment) {
184+
if (isNewQueryExpr(assignment)) {
185+
return assignment.lValue().getText();
186+
}
187+
return computeObjectQueryFromLValue(assignment.lValue());
188+
}
189+
190+
private static boolean isNewQueryExpr(BSLParser.AssignmentContext assignment) {
191+
return Trees.findAllRuleNodes(assignment, BSLParser.RULE_newExpression).stream()
192+
.filter(BSLParser.NewExpressionContext.class::isInstance)
193+
.map(BSLParser.NewExpressionContext.class::cast)
194+
.anyMatch(ctx -> QUERY_PATTERN.matcher(ctx.typeName().getText()).matches());
195+
}
196+
197+
private static String computeObjectQueryFromLValue(BSLParser.LValueContext lValue) {
198+
if (lValue.acceptor() == null) {
199+
return lValue.getText();
200+
}
201+
return Optional.of(lValue.acceptor())
202+
.map(BSLParser.AcceptorContext::accessProperty)
203+
.map(BSLParser.AccessPropertyContext::IDENTIFIER)
204+
.map(ParseTree::getText)
205+
.filter(s -> QUERY_TEXT_PATTERN.matcher(s).matches())
206+
.orElse("");
207+
}
208+
209+
private static Optional<ParameterContext> validateSetParameterCallForName(BSLParser.CallStatementContext callStatementContext,
210+
String queryVarName, List<ParameterContext> params) {
211+
final var ctx = Optional.of(callStatementContext);
212+
final var used = ctx
213+
.map(BSLParser.CallStatementContext::IDENTIFIER)
214+
.map(ParseTree::getText)
215+
.filter(queryVarName::equalsIgnoreCase)
216+
.isPresent();
217+
if (!used) {
218+
return Optional.empty();
219+
}
220+
return ctx.map(BSLParser.CallStatementContext::accessCall)
221+
.map(BSLParser.AccessCallContext::methodCall)
222+
.map(BSLParser.MethodCallContext::doCall)
223+
.map(BSLParser.DoCallContext::callParamList)
224+
.map(BSLParser.CallParamListContext::callParam)
225+
.filter(callParamContexts -> callParamContexts.size() == SET_PARAMETER_PARAMS_COUNT)
226+
.map(callParamContexts -> callParamContexts.get(0))
227+
.map(BSLParser.CallParamContext::expression)
228+
.map(BSLParser.ExpressionContext::member)
229+
.filter(memberContexts -> !memberContexts.isEmpty())
230+
.map(memberContexts -> memberContexts.get(0))
231+
.map(BSLParser.MemberContext::constValue)
232+
.map(BSLParserRuleContext::getText)
233+
.flatMap(s -> params.stream().filter(param -> ("\"" + param.name.getText() + "\"").equalsIgnoreCase(s)).findFirst());
234+
}
235+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* This file is a part of BSL Language Server.
3+
*
4+
* Copyright (c) 2018-2021
5+
* Alexey Sosnoviy <[email protected]>, Nikita Gryzlov <[email protected]> and contributors
6+
*
7+
* SPDX-License-Identifier: LGPL-3.0-or-later
8+
*
9+
* BSL Language Server is free software; you can redistribute it and/or
10+
* modify it under the terms of the GNU Lesser General Public
11+
* License as published by the Free Software Foundation; either
12+
* version 3.0 of the License, or (at your option) any later version.
13+
*
14+
* BSL Language Server is distributed in the hope that it will be useful,
15+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17+
* Lesser General Public License for more details.
18+
*
19+
* You should have received a copy of the GNU Lesser General Public
20+
* License along with BSL Language Server.
21+
*/
22+
package com.github._1c_syntax.bsl.languageserver.diagnostics;
23+
24+
import com.github._1c_syntax.bsl.languageserver.util.TestUtils;
25+
import org.eclipse.lsp4j.Diagnostic;
26+
import org.junit.jupiter.api.Test;
27+
28+
import java.util.List;
29+
30+
import static com.github._1c_syntax.bsl.languageserver.util.Assertions.assertThat;
31+
32+
class MissingQueryParameterDiagnosticTest extends AbstractDiagnosticTest<MissingQueryParameterDiagnostic> {
33+
MissingQueryParameterDiagnosticTest() {
34+
super(MissingQueryParameterDiagnostic.class);
35+
}
36+
37+
@Test
38+
void test() {
39+
40+
List<Diagnostic> diagnostics = getDiagnostics();
41+
42+
assertThat(diagnostics, true)
43+
.hasSize(1)
44+
.hasRange(7, 34, 43)
45+
;
46+
47+
}
48+
49+
@Test
50+
void testWithIsNullOperatorInsideWhere() {
51+
var sample =
52+
"\tЗапрос1 = Новый Запрос(\n" +
53+
"\t\t\"ВЫБРАТЬ\n" +
54+
"\t\t|\tСправочник1.Ссылка КАК Ссылка\n" +
55+
"\t\t|ИЗ\n" +
56+
"\t\t|\tСправочник.Справочник1 КАК Справочник1\n" +
57+
"\t\t|ГДЕ\n" +
58+
"\t\t|\tСправочник1.Ссылка = &Параметр\");\n" +
59+
"\n" +
60+
"\tРезультатЗапроса = Запрос1.Выполнить();\n";
61+
62+
var documentContext = TestUtils.getDocumentContext(sample);
63+
var diagnostics = getDiagnostics(documentContext);
64+
65+
assertThat(diagnostics).hasSize(1);
66+
}
67+
68+
@Test
69+
void testWithIsNullOperatorInsideWhereNoError() {
70+
var sample =
71+
"\tЗапрос1 = Новый Запрос(\n" +
72+
"\t\t\"ВЫБРАТЬ\n" +
73+
"\t\t|\tСправочник1.Ссылка КАК Ссылка\n" +
74+
"\t\t|ИЗ\n" +
75+
"\t\t|\tСправочник.Справочник1 КАК Справочник1\n" +
76+
"\t\t|ГДЕ\n" +
77+
"\t\t|\tСправочник1.Ссылка = &Параметр\");\n" +
78+
"Запрос1.УстановитьПараметр(\"Параметр\", Ссылка);\n" +
79+
"\tРезультатЗапроса = Запрос1.Выполнить();\n";
80+
81+
var documentContext = TestUtils.getDocumentContext(sample);
82+
var diagnostics = getDiagnostics(documentContext);
83+
84+
assertThat(diagnostics).isEmpty();
85+
}
86+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
Процедура ПерваяОшибка()
2+
3+
Запрос1 = Новый Запрос("ВЫБРАТЬ
4+
| Справочник1.Ссылка КАК Ссылка
5+
|ИЗ
6+
| Справочник.Справочник1 КАК Справочник1
7+
|ГДЕ
8+
| Справочник1.Ссылка = &Параметр");
9+
10+
РезультатЗапроса = Запрос1.Выполнить();
11+
12+
КонецПроцедуры
13+
14+
Процедура Вторая_НетОшибки()
15+
16+
Запрос2 = Новый Запрос("ВЫБРАТЬ
17+
| Справочник1.Ссылка КАК Ссылка
18+
|ИЗ
19+
| Справочник.Справочник1 КАК Справочник1
20+
|ГДЕ
21+
| Справочник1.Ссылка = &Параметр");
22+
23+
Запрос2.УстановитьПараметр("Параметр", Ссылка);
24+
25+
РезультатЗапроса = Запрос2.Выполнить(); // нет ошибки
26+
КонецПроцедуры

0 commit comments

Comments
 (0)