diff --git a/bundles/com.e1c.v8codestyle.bsl/src/com/e1c/v8codestyle/bsl/check/LocalizableRegistry.java b/bundles/com.e1c.v8codestyle.bsl/src/com/e1c/v8codestyle/bsl/check/LocalizableRegistry.java new file mode 100644 index 000000000..1862076d1 --- /dev/null +++ b/bundles/com.e1c.v8codestyle.bsl/src/com/e1c/v8codestyle/bsl/check/LocalizableRegistry.java @@ -0,0 +1,198 @@ +/** + * + */ +package com.e1c.v8codestyle.bsl.check; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.eclipse.xtext.util.Pair; +import org.eclipse.xtext.util.Tuples; + +import com._1c.g5.v8.dt.lcore.util.CaseInsensitiveString; +import com._1c.g5.v8.dt.platform.IEObjectTypeNames; +import com.google.inject.Singleton; + +/** + * @author Dmitriy Marmyshev + * + */ +@Singleton +public class LocalizableRegistry +{ + + private Map> staticInvocation; + + private Map, Collection> dynamicInvocation; + + private Map> dynamicProperties; + + private volatile boolean initialized; + + public Collection getStaticInvocationParameters(String methodName) + { + if (methodName == null) + { + return Set.of(); + } + checkInit(); + + return staticInvocation.getOrDefault(new CaseInsensitiveString(methodName), Set.of()); + } + + public Collection getDynamicTypesForMethod(String methodName, int index) + { + if (methodName == null || index < 0) + { + return Set.of(); + } + + checkInit(); + + return dynamicInvocation.getOrDefault(Tuples.create(new CaseInsensitiveString(methodName), index), Set.of()); + } + + public Collection getDynamicTypesForProperty(String propertyName) + { + if (propertyName == null) + { + return Set.of(); + } + + checkInit(); + + return dynamicProperties.getOrDefault(new CaseInsensitiveString(propertyName), Set.of()); + } + + private void checkInit() + { + if (initialized) + { + return; + } + + init(); + } + + private synchronized void init() + { + if (initialized) + { + return; + } + + initStaticInvocation(); + initDynamicInvocation(); + initDynamicProperties(); + + initialized = true; + } + + private void initStaticInvocation() + { + // Global context method name and index of localizable string parameter + Map> invocations = new HashMap<>(); + invocations.put(new CaseInsensitiveString("ПоказатьВопрос"), Set.of(1, 5)); //$NON-NLS-1$ + invocations.put(new CaseInsensitiveString("ShowQueryBox"), Set.of(1, 5)); //$NON-NLS-1$ + invocations.put(new CaseInsensitiveString("Вопрос"), Set.of(0, 4)); //$NON-NLS-1$ + invocations.put(new CaseInsensitiveString("DoQueryBox"), Set.of(0, 4)); //$NON-NLS-1$ + invocations.put(new CaseInsensitiveString("Сообщить"), Set.of(0)); //$NON-NLS-1$ + invocations.put(new CaseInsensitiveString("Message"), Set.of(0)); //$NON-NLS-1$ + invocations.put(new CaseInsensitiveString("Состояние"), Set.of(0, 2)); //$NON-NLS-1$ + invocations.put(new CaseInsensitiveString("Status"), Set.of(0, 2)); //$NON-NLS-1$ + invocations.put(new CaseInsensitiveString("ПоказатьОповещениеПользователя"), Set.of(0, 2)); //$NON-NLS-1$ + invocations.put(new CaseInsensitiveString("ShowUserNotification"), Set.of(0, 2)); //$NON-NLS-1$ + invocations.put(new CaseInsensitiveString("ПоказатьПредупреждение"), Set.of(1, 3)); //$NON-NLS-1$ + invocations.put(new CaseInsensitiveString("ShowMessageBox"), Set.of(1, 3)); //$NON-NLS-1$ + invocations.put(new CaseInsensitiveString("Предупреждение"), Set.of(0, 2)); //$NON-NLS-1$ + invocations.put(new CaseInsensitiveString("DoMessageBox"), Set.of(0, 2)); //$NON-NLS-1$ + + staticInvocation = Map.copyOf(invocations); + } + + private void initDynamicInvocation() + { + //@formatter:off + + // Object's method name and index of localizable string parameter, and collection of object's type names + dynamicInvocation = Map.of( + Tuples.create(new CaseInsensitiveString("Добавить"), 1), Set.of(IEObjectTypeNames.VALUE_LIST), //$NON-NLS-1$ + Tuples.create(new CaseInsensitiveString("Add"), 1), Set.of(IEObjectTypeNames.VALUE_LIST) //$NON-NLS-1$ + ); + + //@formatter:on + + } + + private void initDynamicProperties() + { + //@formatter:off + + // Types that contains property Title + Set titleTypes = Set.of( + IEObjectTypeNames.FORM_FIELD, + IEObjectTypeNames.FORM_GROUP, + IEObjectTypeNames.FORM_TABLE, + IEObjectTypeNames.FORM_DECORATION, + IEObjectTypeNames.FORM_COMMAND, + "FormAttribute", //$NON-NLS-1$ + "FormItemAddition", //$NON-NLS-1$ + IEObjectTypeNames.FORM_BUTTON, + IEObjectTypeNames.CLIENT_APPLICATION_FORM, + "ConditionalAppearenceItem", //$NON-NLS-1$ + "AppearenceSettingItem", //$NON-NLS-1$ + "CollaborationSystemConversation", //$NON-NLS-1$ + "DeliverableNotification", //$NON-NLS-1$ + "RepresentableDocumentBatch", //$NON-NLS-1$ + "HTMLDocument", //$NON-NLS-1$ + "ValueTableColumn", //$NON-NLS-1$ + "ValueTreeColumn", //$NON-NLS-1$ + "DataCompositionAreaTemplateValueCollectionHeaderCell", //$NON-NLS-1$ + IEObjectTypeNames.DATA_COMPOSITION_USER_FIELD_EXPRESSION, + IEObjectTypeNames.DATA_COMPOSITION_USER_FIELD_CASE, + IEObjectTypeNames.DATA_COMPOSITION_SELECTED_FIELD_GROUP, + IEObjectTypeNames.DATA_COMPOSITION_SELECTED_FIELD, + IEObjectTypeNames.DATA_COMPOSITION_FILTER_AVAILABLE_FIELD, + "NestedDataCompositionSchema", //$NON-NLS-1$ + "DataCompositionSchemaParameter", //$NON-NLS-1$ + "DataCompositionSchemaNestedDataSet", //$NON-NLS-1$ + "DataCompositionSchemaDataSetFieldFolder", //$NON-NLS-1$ + "DataCompositionSchemaDataSetField", //$NON-NLS-1$ + "DataCompositionSchemaCalculatedField", //$NON-NLS-1$ + IEObjectTypeNames.DATA_ANALYSIS_PARAMETERS, + "GanttChartPlotArea", //$NON-NLS-1$ + "FileDialog" //$NON-NLS-1$ + ); + + // Types that contains property ToolTip + // TODO add all types with tooltip + Set toolTipTypes = Set.of( + IEObjectTypeNames.FORM_FIELD, + IEObjectTypeNames.FORM_GROUP, + IEObjectTypeNames.FORM_TABLE, + IEObjectTypeNames.FORM_DECORATION, + IEObjectTypeNames.FORM_COMMAND, + "FormItemAddition", //$NON-NLS-1$ + "DateAppearence" //$NON-NLS-1$ + ); + + // TODO add types of graphical scheme with Description + + // TODO add types of DCS with Presentation + + // Localizable property name, and collection of types + dynamicProperties = Map.of( + new CaseInsensitiveString("Подсказка"), toolTipTypes, //$NON-NLS-1$ + new CaseInsensitiveString("ToolTip"), toolTipTypes, //$NON-NLS-1$ + new CaseInsensitiveString("ПодсказкаВвода"), Set.of("FormFieldExtenstionForTextBox"), //$NON-NLS-1$ + new CaseInsensitiveString("InputHint"), Set.of("FormFieldExtenstionForTextBox"), //$NON-NLS-1$ + new CaseInsensitiveString("Заголовок"), titleTypes, //$NON-NLS-1$ + new CaseInsensitiveString("Title"), titleTypes //$NON-NLS-1$ + ); + //@formatter:on + + } + +} diff --git a/bundles/com.e1c.v8codestyle.bsl/src/com/e1c/v8codestyle/bsl/check/NstrLocalizableStringCheck.java b/bundles/com.e1c.v8codestyle.bsl/src/com/e1c/v8codestyle/bsl/check/NstrLocalizableStringCheck.java new file mode 100644 index 000000000..bafc11054 --- /dev/null +++ b/bundles/com.e1c.v8codestyle.bsl/src/com/e1c/v8codestyle/bsl/check/NstrLocalizableStringCheck.java @@ -0,0 +1,168 @@ +/******************************************************************************* + * 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 + *******************************************************************************/ +/** + * + */ +package com.e1c.v8codestyle.bsl.check; + +import static com._1c.g5.v8.dt.bsl.model.BslPackage.Literals.STRING_LITERAL; +import static com._1c.g5.v8.dt.bsl.model.BslPackage.Literals.STRING_LITERAL__LINES; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.xtext.EcoreUtil2; + +import com._1c.g5.v8.dt.bsl.model.DynamicFeatureAccess; +import com._1c.g5.v8.dt.bsl.model.FeatureAccess; +import com._1c.g5.v8.dt.bsl.model.Invocation; +import com._1c.g5.v8.dt.bsl.model.SimpleStatement; +import com._1c.g5.v8.dt.bsl.model.StaticFeatureAccess; +import com._1c.g5.v8.dt.bsl.model.StringLiteral; +import com._1c.g5.v8.dt.bsl.resource.TypesComputer; +import com._1c.g5.v8.dt.common.StringUtils; +import com._1c.g5.v8.dt.mcore.Environmental; +import com._1c.g5.v8.dt.mcore.TypeItem; +import com._1c.g5.v8.dt.mcore.util.McoreUtil; +import com.e1c.g5.v8.dt.check.CheckComplexity; +import com.e1c.g5.v8.dt.check.ICheckParameters; +import com.e1c.g5.v8.dt.check.components.BasicCheck; +import com.e1c.g5.v8.dt.check.settings.IssueSeverity; +import com.e1c.g5.v8.dt.check.settings.IssueType; +import com.e1c.v8codestyle.check.CommonSenseCheckExtension; +import com.e1c.v8codestyle.internal.bsl.BslPlugin; +import com.google.inject.Inject; + +/** + * @author Dmitriy Marmyshev + * + */ +public class NstrLocalizableStringCheck + extends BasicCheck +{ + private static final String CHECK_ID = "bsl-localizable-string"; //$NON-NLS-1$ + + private final TypesComputer typesComputer; + + private final LocalizableRegistry localizableRegistry; + + @Inject + public NstrLocalizableStringCheck(TypesComputer typesComputer, LocalizableRegistry localizableRegistry) + { + this.typesComputer = typesComputer; + this.localizableRegistry = localizableRegistry; + } + + @Override + public String getCheckId() + { + return CHECK_ID; + } + + @Override + protected void configureCheck(CheckConfigurer builder) + { + builder.title("String literal should be localizable with NStr") + .description("String literal should be localizable with NStr") + .complexity(CheckComplexity.NORMAL) + .severity(IssueSeverity.MINOR) + .issueType(IssueType.UI_STYLE) + .extension(new CommonSenseCheckExtension(getCheckId(), BslPlugin.PLUGIN_ID)) + .module() + .checkedObjectType(STRING_LITERAL); + + } + + @Override + protected void check(Object object, ResultAcceptor resultAceptor, ICheckParameters parameters, + IProgressMonitor monitor) + { + StringLiteral literal = (StringLiteral)object; + + if (literal.getLines().size() == 1 && StringUtils.isBlank(literal.lines(true).get(0))) + { + // skip empty lines + return; + } + + EObject parent = literal.eContainer(); + + if (parent instanceof Invocation && isLocalizableParameter((Invocation)parent, literal, monitor) + || parent instanceof SimpleStatement && ((SimpleStatement)parent).getLeft() instanceof DynamicFeatureAccess + && !monitor.isCanceled() + && isLocalizableProperty((DynamicFeatureAccess)((SimpleStatement)parent).getLeft(), monitor)) + { + resultAceptor.addIssue("Localizable string should be in NStr()", STRING_LITERAL__LINES); + } + + } + + private boolean isLocalizableParameter(Invocation inv, EObject parameter, IProgressMonitor monitor) + { + FeatureAccess method = inv.getMethodAccess(); + if (StringUtils.isBlank(method.getName())) + { + return false; + } + + if (method instanceof StaticFeatureAccess && !monitor.isCanceled()) + { + Collection params = localizableRegistry.getStaticInvocationParameters(method.getName()); + if (!params.isEmpty()) + { + int index = inv.getParams().indexOf(parameter); + return params.contains(index); + } + } + else if (method instanceof DynamicFeatureAccess && !monitor.isCanceled()) + { + int index = inv.getParams().indexOf(parameter); + DynamicFeatureAccess dfa = (DynamicFeatureAccess)method; + Collection typeNames = localizableRegistry.getDynamicTypesForMethod(dfa.getName(), index); + if (!typeNames.isEmpty() && !monitor.isCanceled()) + { + Environmental env = EcoreUtil2.getContainerOfType(dfa, Environmental.class); + List types = typesComputer.computeTypes(dfa.getSource(), env.environments()); + return !types.isEmpty() && types.stream() + .map(McoreUtil::getTypeName) + .filter(Objects::nonNull) + .anyMatch(typeNames::contains); + } + } + return false; + } + + private boolean isLocalizableProperty(DynamicFeatureAccess property, IProgressMonitor monitor) + { + String name = property.getName(); + if (StringUtils.isBlank(name)) + { + return false; + } + Collection typeNames = localizableRegistry.getDynamicTypesForProperty(name); + + if (!typeNames.isEmpty() && !monitor.isCanceled()) + { + Environmental env = EcoreUtil2.getContainerOfType(property, Environmental.class); + List types = typesComputer.computeTypes(property.getSource(), env.environments()); + return !types.isEmpty() + && types.stream().map(McoreUtil::getTypeName).filter(Objects::nonNull).anyMatch(typeNames::contains); + } + + return false; + } + +}