diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ed740692..67ac5955c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ - Добавление типизированного значения в не типизированную коллекцию #### Запросы +- В запросе отсутствует проверка на NULL для поля, которое может потенциально содержать NULL #### Права ролей diff --git a/bundles/com.e1c.v8codestyle.ql/markdown/ql-query-field-isnull.md b/bundles/com.e1c.v8codestyle.ql/markdown/ql-query-field-isnull.md new file mode 100644 index 000000000..25692c994 --- /dev/null +++ b/bundles/com.e1c.v8codestyle.ql/markdown/ql-query-field-isnull.md @@ -0,0 +1,50 @@ +# The query is missing a NULL check for a field that could potentially contain NULL. + +2. When you order a query result by fields that might contain NULL, take into account that the sorting priority of NULL varies in different DBMS. + +## Noncompliant Code Example + +```bsl +SELECT + CatalogProducts.Ref AS ProductRef, + InventoryBalance.QuantityBalance AS QuantityBalance +FROM + Catalog.Products AS CatalogProducts + LEFT JOIN AccumulationRegister.Inventory.Balance AS InventoryBalance + BY (InventoryBalance.Products = CatalogProducts.Ref) + +ORDER BY + QuantityBalance +``` + +## Compliant Solution + +```bsl +SELECT + CatalogProducts.Ref AS ProductRef, + ISNULL(InventoryBalance.QuantityBalance, 0) AS QuantityBalance +FROM + Catalog.Products AS CatalogProducts + LEFT JOIN AccumulationRegister.Inventory.Balance AS InventoryBalance + BY (InventoryBalance.Products = CatalogProducts.Ref) + +ORDER BY + QuantityBalance; + +SELECT + CatalogProducts.Ref AS ProductRef, + CASE WHEN InventoryBalance.QuantityBalance IS NOT NULL THEN 0 ELSE InventoryBalance.QuantityBalance AS QuantityBalance +FROM + Catalog.Products AS CatalogProducts + LEFT JOIN AccumulationRegister.Inventory.Balance AS InventoryBalance + BY (InventoryBalance.Products = CatalogProducts.Ref) + +ORDER BY + QuantityBalance; +``` + +## See + +- [Ordering query results](https://support.1ci.com/hc/en-us/articles/360011120859-Ordering-query-results) +- [Using the CASE operation](https://support.1ci.com/hc/en-us/articles/360007870794-Query-language#using_the_isnull___function) +- [Appendix 8. Features of operating with different DBMS](https://support.1ci.com/hc/en-us/articles/6347699838098-8-3-IBM-Db2) \ No newline at end of file diff --git a/bundles/com.e1c.v8codestyle.ql/markdown/ru/ql-query-field-isnull.md b/bundles/com.e1c.v8codestyle.ql/markdown/ru/ql-query-field-isnull.md new file mode 100644 index 000000000..e724ff629 --- /dev/null +++ b/bundles/com.e1c.v8codestyle.ql/markdown/ru/ql-query-field-isnull.md @@ -0,0 +1,55 @@ +# В запросе отсутствует проверка на NULL для поля, которое может потенциально содержать NULL. + +1.2. При сортировке по полю запроса, которое может потенциально содержать NULL, следует учитывать, что в разных СУБД порядок сортировки по этому полю может отличаться. + +## Неправильно + +```bsl +ВЫБРАТЬ + СправочникНоменклатура.Ссылка КАК НоменклатураСсылка, + ЗапасыОстатки.КоличествоОстаток КАК КоличествоОстаток +ИЗ + Справочник.Номенклатура КАК СправочникНоменклатура + ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.Запасы.Остатки КАК +ЗапасыОстатки + ПО (ЗапасыОстатки.Номенклатура = СправочникНоменклатура.Ссылка) + +УПОРЯДОЧИТЬ ПО + КоличествоОстаток +``` + +## Правильно + +При использовании в тексте запроса оператора ПОДОБНО и СПЕЦСИМВОЛ допустимо использовать только константные строковые литералы или параметры запроса. + +```bsl +ВЫБРАТЬ + СправочникНоменклатура.Ссылка КАК НоменклатураСсылка, + ЕСТЬNULL(ЗапасыОстатки.КоличествоОстаток, 0) КАК КоличествоОстаток +ИЗ + Справочник.Номенклатура КАК СправочникНоменклатура + ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.Запасы.Остатки КАК +ЗапасыОстатки + ПО (ЗапасыОстатки.Номенклатура = СправочникНоменклатура.Ссылка) + +УПОРЯДОЧИТЬ ПО + КоличествоОстаток; + +ВЫБРАТЬ + СправочникНоменклатура.Ссылка КАК НоменклатураСсылка, + ВЫБОР КОГДА ЗапасыОстатки.КоличествоОстаток ЕСТЬ НЕ NULL ТОГДА 0 ИНАЧЕ ЗапасыОстатки.КоличествоОстаток КАК КоличествоОстаток +ИЗ + Справочник.Номенклатура КАК СправочникНоменклатура + ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.Запасы.Остатки КАК +ЗапасыОстатки + ПО (ЗапасыОстатки.Номенклатура = СправочникНоменклатура.Ссылка) + +УПОРЯДОЧИТЬ ПО + КоличествоОстаток; +``` + +## См. + +- [Упорядочивание результатов запроса](https://its.1c.ru/db/v8std#content:412:hdoc:1.2) +- [Использование операции ВЫБОР](https://its.1c.ru/db/metod8dev/content/2653/hdoc) +- [Приложение 8. Особенности работы с различными СУБД.](http://its.1c.ru/db/v83doc#bookmark:dev:TI000001285) \ No newline at end of file diff --git a/bundles/com.e1c.v8codestyle.ql/plugin.xml b/bundles/com.e1c.v8codestyle.ql/plugin.xml index 10134d432..b357d5fb9 100644 --- a/bundles/com.e1c.v8codestyle.ql/plugin.xml +++ b/bundles/com.e1c.v8codestyle.ql/plugin.xml @@ -54,6 +54,10 @@ category="com.e1c.v8codestyle.ql" class="com.e1c.v8codestyle.ql.check.ConstantsInBinaryOperationCheck"> + + diff --git a/bundles/com.e1c.v8codestyle.ql/src/com/e1c/v8codestyle/ql/check/Messages.java b/bundles/com.e1c.v8codestyle.ql/src/com/e1c/v8codestyle/ql/check/Messages.java index 354548dd4..f777804d8 100644 --- a/bundles/com.e1c.v8codestyle.ql/src/com/e1c/v8codestyle/ql/check/Messages.java +++ b/bundles/com.e1c.v8codestyle.ql/src/com/e1c/v8codestyle/ql/check/Messages.java @@ -41,6 +41,9 @@ final class Messages public static String JoinToSubQuery_description; public static String JoinToSubQuery_Query_join_to_sub_query_not_allowed; public static String JoinToSubQuery_title; + public static String QueryFieldIsNullCheck_description; + public static String QueryFieldIsNullCheck_Query_missing_NULL_check_for_field_potentially_contain_NULL; + public static String QueryFieldIsNullCheck_title; public static String TempTableHasIndex_description; public static String TempTableHasIndex_Exclude_table_name_pattern; public static String TempTableHasIndex_New_temporary_table_should_have_indexes; diff --git a/bundles/com.e1c.v8codestyle.ql/src/com/e1c/v8codestyle/ql/check/QueryFieldIsNullCheck.java b/bundles/com.e1c.v8codestyle.ql/src/com/e1c/v8codestyle/ql/check/QueryFieldIsNullCheck.java new file mode 100644 index 000000000..d5cfc60a4 --- /dev/null +++ b/bundles/com.e1c.v8codestyle.ql/src/com/e1c/v8codestyle/ql/check/QueryFieldIsNullCheck.java @@ -0,0 +1,197 @@ +/******************************************************************************* + * Copyright (C) 2022, 1C-Soft LLC and others. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * 1C-Soft LLC - initial API and implementation + * Denis Maslennikov - issue #163 + *******************************************************************************/ +package com.e1c.v8codestyle.ql.check; + +import static com._1c.g5.v8.dt.ql.model.QlPackage.Literals.COMMON_EXPRESSION__CONTENT; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.xtext.EcoreUtil2; + +import com._1c.g5.v8.dt.ql.model.AbstractExpression; +import com._1c.g5.v8.dt.ql.model.CaseBody; +import com._1c.g5.v8.dt.ql.model.CaseOperationExpression; +import com._1c.g5.v8.dt.ql.model.CommonExpression; +import com._1c.g5.v8.dt.ql.model.FunctionExpression; +import com._1c.g5.v8.dt.ql.model.FunctionInvocationExpression; +import com._1c.g5.v8.dt.ql.model.IsNullOperatorExpression; +import com._1c.g5.v8.dt.ql.model.MultiPartCommonExpression; +import com._1c.g5.v8.dt.ql.model.QuerySchemaExpression; +import com._1c.g5.v8.dt.ql.model.QuerySchemaOperator; +import com._1c.g5.v8.dt.ql.model.QuerySchemaOrderExpression; +import com._1c.g5.v8.dt.ql.model.QuerySchemaSelectQuery; +import com.e1c.g5.v8.dt.check.CheckComplexity; +import com.e1c.g5.v8.dt.check.ICheckParameters; +import com.e1c.g5.v8.dt.check.settings.IssueSeverity; +import com.e1c.g5.v8.dt.check.settings.IssueType; +import com.e1c.g5.v8.dt.ql.check.QlBasicDelegateCheck; +import com.e1c.v8codestyle.check.StandardCheckExtension; +import com.e1c.v8codestyle.internal.ql.CorePlugin; + +/** + * This class checks if null is checked when sorting by query field. + * + * @author Denis Maslennikov + */ +public class QueryFieldIsNullCheck + extends QlBasicDelegateCheck +{ + + private static final String CHECK_ID = "ql-query-field-isnull"; //$NON-NLS-1$ + private static final String METHOD_DELIMITER = ","; //$NON-NLS-1$ + private static final String ISNULL_METHODS = "ISNULL,ЕСТЬNULL"; //$NON-NLS-1$ + + @Override + public String getCheckId() + { + return CHECK_ID; + } + + @Override + protected void configureCheck(CheckConfigurer builder) + { + builder.title(Messages.QueryFieldIsNullCheck_title) + .description(Messages.QueryFieldIsNullCheck_description) + .complexity(CheckComplexity.NORMAL) + .severity(IssueSeverity.MINOR) + .issueType(IssueType.PORTABILITY) + .extension(new StandardCheckExtension(412, getCheckId(), CorePlugin.PLUGIN_ID)) + .delegate(QuerySchemaSelectQuery.class); + } + + @Override + protected void checkQlObject(EObject object, QueryOwner owner, IQlResultAcceptor resultAceptor, + ICheckParameters parameters, IProgressMonitor monitor) + { + QuerySchemaSelectQuery sourceTable = (QuerySchemaSelectQuery)object; + + if (monitor.isCanceled()) + { + return; + } + + List orderFields = getOrderFields(sourceTable); + List operators = sourceTable.getOperators(); + for (QuerySchemaOperator operator : operators) + { + List fields = operator.getSelectFields(); + for (QuerySchemaExpression field : fields) + { + AbstractExpression abstractExpression = field.getExpression(); + String fieldName = field.getAlias(); + if (abstractExpression instanceof FunctionInvocationExpression) + { + removeFieldsWithIsNullMethod((FunctionInvocationExpression)abstractExpression, orderFields, + fieldName); + } + if (abstractExpression instanceof CaseOperationExpression) + { + removeFieldsWithWhenExpression((CaseOperationExpression)abstractExpression, orderFields, fieldName); + } + } + } + for (CommonExpression orderField : orderFields) + { + String message = Messages.QueryFieldIsNullCheck_Query_missing_NULL_check_for_field_potentially_contain_NULL; + resultAceptor.addIssue(message, orderField, COMMON_EXPRESSION__CONTENT); + } + } + + private void removeFieldsWithWhenExpression(CaseOperationExpression caseInvocationExpression, + List orderFields, String fieldName) + { + List caseBodies = caseInvocationExpression.getBody(); + for (CaseBody caseBody : caseBodies) + { + AbstractExpression whenExpression = caseBody.getWhen(); + if (whenExpression instanceof IsNullOperatorExpression) + { + IsNullOperatorExpression nullOperator = (IsNullOperatorExpression)whenExpression; + for (EObject operatorContent : nullOperator.eContents()) + { + if (operatorContent instanceof CommonExpression) + { + removeFieldByName(orderFields, (CommonExpression)operatorContent, fieldName); + } + } + } + } + } + + private void removeFieldsWithIsNullMethod(FunctionInvocationExpression functionInvocationExpression, + List orderFields, String fieldName) + { + FunctionExpression f = functionInvocationExpression.getFunctionType(); + List isNullMethods = getIsNullMethods(); + + if (isNullMethods.contains(f.getName())) + { + List params = functionInvocationExpression.getParams(); + + if (params.get(0) instanceof CommonExpression) + { + removeFieldByName(orderFields, (CommonExpression)params.get(0), fieldName); + } + + } + } + + private void removeFieldByName(List orderFields, CommonExpression commonExpression, + String fieldName) + { + String paramName = commonExpression.getFullContent(); + List unique = new ArrayList<>(); + unique.addAll(orderFields); + for (CommonExpression orderField : unique) + { + String fieldFullName = orderField.getFullContent(); + if (paramName.equals(fieldFullName) || fieldName.equals(fieldFullName)) + { + orderFields.remove(orderField); + } + } + } + + private List getOrderFields(QuerySchemaSelectQuery sourceTable) + { + List orderExpressions = sourceTable.getOrderExpressions(); + List orderFields = new ArrayList<>(); + + for (QuerySchemaOrderExpression orderExpression : orderExpressions) + { + List commonExpressions = + EcoreUtil2.getAllContentsOfType(orderExpression, CommonExpression.class); + orderFields.addAll(commonExpressions); + for (CommonExpression commonExpression : commonExpressions) + { + if (commonExpression instanceof MultiPartCommonExpression) + { + orderFields.remove(((MultiPartCommonExpression)commonExpression).getSourceTable()); + } + } + } + return orderFields; + } + + private List getIsNullMethods() + { + List isNullMethods = Arrays.asList(ISNULL_METHODS.split(METHOD_DELIMITER)); + isNullMethods.replaceAll(String::trim); + return isNullMethods; + } +} diff --git a/bundles/com.e1c.v8codestyle.ql/src/com/e1c/v8codestyle/ql/check/messages.properties b/bundles/com.e1c.v8codestyle.ql/src/com/e1c/v8codestyle/ql/check/messages.properties index 59d8486cc..0c3a61845 100644 --- a/bundles/com.e1c.v8codestyle.ql/src/com/e1c/v8codestyle/ql/check/messages.properties +++ b/bundles/com.e1c.v8codestyle.ql/src/com/e1c/v8codestyle/ql/check/messages.properties @@ -50,6 +50,12 @@ JoinToSubQuery_description = Query join with sub query JoinToSubQuery_title = Query join with sub query +QueryFieldIsNullCheck_description = The query is missing a NULL check for a field that could potentially contain NULL + +QueryFieldIsNullCheck_Query_missing_NULL_check_for_field_potentially_contain_NULL = The query is missing a NULL check for a field that could potentially contain NULL + +QueryFieldIsNullCheck_title = The query is missing a NULL check for a field that could potentially contain NULL + TempTableHasIndex_Exclude_table_name_pattern = Exclude table name pattern TempTableHasIndex_New_temporary_table_should_have_indexes = New temporary table should have indexes diff --git a/bundles/com.e1c.v8codestyle.ql/src/com/e1c/v8codestyle/ql/check/messages_ru.properties b/bundles/com.e1c.v8codestyle.ql/src/com/e1c/v8codestyle/ql/check/messages_ru.properties index d50972682..5b4e090a9 100644 --- a/bundles/com.e1c.v8codestyle.ql/src/com/e1c/v8codestyle/ql/check/messages_ru.properties +++ b/bundles/com.e1c.v8codestyle.ql/src/com/e1c/v8codestyle/ql/check/messages_ru.properties @@ -51,6 +51,12 @@ JoinToSubQuery_description = Соединение запроса с подзап JoinToSubQuery_title = Соединение запроса с подзапросом +QueryFieldIsNullCheck_description = В запросе отсутствует проверка на NULL для поля, которое может потенциально содержать NULL + +QueryFieldIsNullCheck_Query_missing_NULL_check_for_field_potentially_contain_NULL = В запросе отсутствует проверка на NULL для поля, которое может потенциально содержать NULL + +QueryFieldIsNullCheck_title = В запросе отсутствует проверка на NULL для поля, которое может потенциально содержать NULL + TempTableHasIndex_Exclude_table_name_pattern = Выражения исключения имени таблицы TempTableHasIndex_New_temporary_table_should_have_indexes = Новая временная таблица должна содержать индексы diff --git a/tests/com.e1c.v8codestyle.ql.itests/resources/ql-query-field-isnull/compliant.ql b/tests/com.e1c.v8codestyle.ql.itests/resources/ql-query-field-isnull/compliant.ql new file mode 100644 index 000000000..47c2b7133 --- /dev/null +++ b/tests/com.e1c.v8codestyle.ql.itests/resources/ql-query-field-isnull/compliant.ql @@ -0,0 +1,35 @@ +ВЫБРАТЬ + СправочникНоменклатура.Ссылка КАК НоменклатураСсылка, + ЕСТЬNULL(ЗапасыОстатки.КоличествоОстаток, 0) КАК КоличествоОстаток +ИЗ + Справочник.Номенклатура КАК СправочникНоменклатура + ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.Запасы.Остатки КАК +ЗапасыОстатки + ПО (ЗапасыОстатки.Номенклатура = СправочникНоменклатура.Ссылка) + +УПОРЯДОЧИТЬ ПО + ЗапасыОстатки.КоличествоОстаток; + +ВЫБРАТЬ + СправочникНоменклатура.Ссылка КАК НоменклатураСсылка, + ЕСТЬNULL(ЗапасыОстатки.КоличествоОстаток, 0) КАК КоличествоОстаток +ИЗ + Справочник.Номенклатура КАК СправочникНоменклатура + ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.Запасы.Остатки КАК +ЗапасыОстатки + ПО (ЗапасыОстатки.Номенклатура = СправочникНоменклатура.Ссылка) + +УПОРЯДОЧИТЬ ПО + КоличествоОстаток; + +ВЫБРАТЬ + СправочникНоменклатура.Ссылка КАК НоменклатураСсылка, + ВЫБОР КОГДА ЗапасыОстатки.КоличествоОстаток ЕСТЬ НЕ NULL ТОГДА 0 ИНАЧЕ ЗапасыОстатки.КоличествоОстаток КАК КоличествоОстаток +ИЗ + Справочник.Номенклатура КАК СправочникНоменклатура + ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.Запасы.Остатки КАК +ЗапасыОстатки + ПО (ЗапасыОстатки.Номенклатура = СправочникНоменклатура.Ссылка) + +УПОРЯДОЧИТЬ ПО + КоличествоОстаток \ No newline at end of file diff --git a/tests/com.e1c.v8codestyle.ql.itests/resources/ql-query-field-isnull/non-compliant.ql b/tests/com.e1c.v8codestyle.ql.itests/resources/ql-query-field-isnull/non-compliant.ql new file mode 100644 index 000000000..a4b7016ed --- /dev/null +++ b/tests/com.e1c.v8codestyle.ql.itests/resources/ql-query-field-isnull/non-compliant.ql @@ -0,0 +1,36 @@ +ВЫБРАТЬ + СправочникНоменклатура.Ссылка КАК НоменклатураСсылка, + ЗапасыОстатки.КоличествоОстаток КАК КоличествоОстаток +ИЗ + Справочник.Номенклатура КАК СправочникНоменклатура + ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.Запасы.Остатки КАК +ЗапасыОстатки + ПО (ЗапасыОстатки.Номенклатура = СправочникНоменклатура.Ссылка) + +УПОРЯДОЧИТЬ ПО + ЗапасыОстатки.КоличествоОстаток; + +ВЫБРАТЬ + СправочникНоменклатура.Ссылка КАК НоменклатураСсылка, + ЗапасыОстатки.КоличествоОстаток КАК КоличествоОстаток +ИЗ + Справочник.Номенклатура КАК СправочникНоменклатура + ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.Запасы.Остатки КАК +ЗапасыОстатки + ПО (ЗапасыОстатки.Номенклатура = СправочникНоменклатура.Ссылка) + +УПОРЯДОЧИТЬ ПО + КоличествоОстаток; + +ВЫБРАТЬ + СправочникНоменклатура.Ссылка КАК НоменклатураСсылка, + ВЫБОР КОГДА ЗапасыОстатки.КоличествоОстаток ЕСТЬ НЕ NULL ТОГДА 0 ИНАЧЕ ЗапасыОстатки.КоличествоОстаток +ИЗ + Справочник.Номенклатура КАК СправочникНоменклатура + ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.Запасы.Остатки КАК +ЗапасыОстатки + ПО (ЗапасыОстатки.Номенклатура = СправочникНоменклатура.Ссылка) + +УПОРЯДОЧИТЬ ПО + КоличествоОстаток; + \ No newline at end of file diff --git a/tests/com.e1c.v8codestyle.ql.itests/src/com/e1c/v8codestyle/ql/check/itests/QueryFieldIsNullTest.java b/tests/com.e1c.v8codestyle.ql.itests/src/com/e1c/v8codestyle/ql/check/itests/QueryFieldIsNullTest.java new file mode 100644 index 000000000..ba3648053 --- /dev/null +++ b/tests/com.e1c.v8codestyle.ql.itests/src/com/e1c/v8codestyle/ql/check/itests/QueryFieldIsNullTest.java @@ -0,0 +1,89 @@ +/******************************************************************************* + * Copyright (C) 2022, 1C-Soft LLC and others. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * 1C-Soft LLC - initial API and implementation + * Denis Maslennikov - issue #163 + *******************************************************************************/ +/** + * + */ +package com.e1c.v8codestyle.ql.check.itests; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.util.List; + +import org.junit.Test; + +import com.e1c.v8codestyle.ql.check.QueryFieldIsNullCheck; +import com.e1c.v8codestyle.ql.check.itests.TestingQlResultAcceptor.QueryMarker; + +/** + * The test for class {@link QueryFieldIsNullCheck}. + * + * @author Denis Maslennikov + */ +public class QueryFieldIsNullTest + extends AbstractQueryTestBase +{ + private static final String PROJECT_NAME = "QlFullDemo"; + private static final String FOLDER = FOLDER_RESOURCE + "ql-query-field-isnull/"; + + public QueryFieldIsNullTest() + { + super(QueryFieldIsNullCheck.class); + } + + @Override + protected String getTestConfigurationName() + { + return PROJECT_NAME; + } + + /** + * Test correct ISNULL check for query field. + * + * @throws Exception the exception + */ + @Test + public void testQueryFieldIsNullCompliant() throws Exception + { + loadQueryAndValidate(FOLDER + "compliant.ql"); + List markers = getQueryMarkers(); + assertEquals(0, markers.size()); + } + + /** + * Test if ISNULL check is absent for query field. + * + * @throws Exception the exception + */ + @Test + public void testQueryFieldIsNullNonCompliant() throws Exception + { + loadQueryAndValidate(FOLDER + "non-compliant.ql"); + List markers = getQueryMarkers(); + assertEquals(3, markers.size()); + + QueryMarker marker = markers.get(0); + assertNotNull(marker.getTarget()); + assertEquals(11, marker.getLineNumber()); + + marker = markers.get(1); + assertNotNull(marker.getTarget()); + assertEquals(23, marker.getLineNumber()); + + marker = markers.get(2); + assertNotNull(marker.getTarget()); + assertEquals(35, marker.getLineNumber()); + + } +}