All Downloads are FREE. Search and download functionalities are using the official Maven repository.

software.amazon.smithy.linters.NoninclusiveTermsValidator Maven / Gradle / Ivy

/*
 * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file is distributed
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

package software.amazon.smithy.linters;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.SourceLocation;
import software.amazon.smithy.model.knowledge.TextIndex;
import software.amazon.smithy.model.knowledge.TextInstance;
import software.amazon.smithy.model.node.NodeMapper;
import software.amazon.smithy.model.traits.Trait;
import software.amazon.smithy.model.validation.AbstractValidator;
import software.amazon.smithy.model.validation.Severity;
import software.amazon.smithy.model.validation.ValidationEvent;
import software.amazon.smithy.model.validation.ValidationUtils;
import software.amazon.smithy.model.validation.ValidatorService;
import software.amazon.smithy.utils.ListUtils;
import software.amazon.smithy.utils.MapUtils;
import software.amazon.smithy.utils.StringUtils;

/**
 * 

Validates that all shape names and values do not contain non-inclusive terms. */ public final class NoninclusiveTermsValidator extends AbstractValidator { static final Map> BUILT_IN_NONINCLUSIVE_TERMS = MapUtils.of( "master", ListUtils.of("primary", "parent", "main"), "slave", ListUtils.of("secondary", "replica", "clone", "child"), "blacklist", ListUtils.of("denyList"), "whitelist", ListUtils.of("allowList") ); public static final class Provider extends ValidatorService.Provider { public Provider() { super(NoninclusiveTermsValidator.class, node -> { NodeMapper mapper = new NodeMapper(); return new NoninclusiveTermsValidator( mapper.deserialize(node, NoninclusiveTermsValidator.Config.class)); }); } } private static final String TRAIT = "Trait"; private static final String SHAPE = "Shape"; private static final String NAMESPACE = "Namespace"; /** * NoninclusiveTermsValidator configuration. */ public static final class Config { private Map> terms = MapUtils.of(); private boolean excludeDefaults; public Map> getTerms() { return terms; } public void setTerms(Map> terms) { this.terms = terms; } public boolean getExcludeDefaults() { return excludeDefaults; } public void setExcludeDefaults(boolean excludeDefaults) { this.excludeDefaults = excludeDefaults; } } private final Map> termsMap; private NoninclusiveTermsValidator(Config config) { Map> termsMapInit = new HashMap<>(BUILT_IN_NONINCLUSIVE_TERMS); if (!config.getExcludeDefaults()) { termsMapInit.putAll(config.getTerms()); termsMap = Collections.unmodifiableMap(termsMapInit); } else { if (config.getTerms().isEmpty()) { //This configuration combination makes the validator a no-op. throw new IllegalArgumentException("Cannot set 'excludeDefaults' to true and leave " + "'terms' empty or unspecified."); } termsMap = Collections.unmodifiableMap(config.getTerms()); } } /** * Runs a full text scan on a given model and stores the resulting TextOccurrences objects. * * Namespaces are checked against a global set per model. * * @param model Model to validate. * @return a list of ValidationEvents found by the implementer of getValidationEvents per the * TextOccurrences provided by this traversal. */ @Override public List validate(Model model) { TextIndex textIndex = TextIndex.of(model); List validationEvents = new ArrayList<>(); for (TextInstance text : textIndex.getTextInstances()) { validationEvents.addAll(getValidationEvents(text)); } return validationEvents; } /** * Generates zero or more @see ValidationEvents and returns them in a collection. * * @param instance text occurrence found in the body of the model */ private Collection getValidationEvents(TextInstance instance) { final Collection events = new ArrayList<>(); for (Map.Entry> termEntry : termsMap.entrySet()) { final String termLower = termEntry.getKey().toLowerCase(); final int startIndex = instance.getText().toLowerCase().indexOf(termLower); if (startIndex != -1) { final String matchedText = instance.getText().substring(startIndex, startIndex + termLower.length()); events.add(constructValidationEvent(instance, termEntry.getValue(), matchedText)); } } return events; } private ValidationEvent constructValidationEvent(TextInstance instance, List replacements, String matchedText) { String replacementAddendum = getReplacementAddendum(matchedText, replacements); switch (instance.getLocationType()) { case NAMESPACE: //Cannot use any warning() overloads because there is no shape associated with the event. return ValidationEvent.builder() .severity(Severity.WARNING) .sourceLocation(SourceLocation.none()) .id(getName() + "." + NAMESPACE + "." + instance.getText() + "." + matchedText.toLowerCase(Locale.US)) .message(String.format("%s namespace uses a non-inclusive term `%s`.%s", instance.getText(), matchedText, replacementAddendum)) .build(); case APPLIED_TRAIT: ValidationEvent validationEvent = warning(instance.getShape(), instance.getTrait().getSourceLocation(), ""); String idiomaticTraitName = Trait.getIdiomaticTraitName(instance.getTrait()); if (instance.getTraitPropertyPath().isEmpty()) { return validationEvent.toBuilder() .message(String.format("'%s' trait has a value that contains a non-inclusive term `%s`.%s", idiomaticTraitName, matchedText, replacementAddendum)) .id(getName() + "." + TRAIT + "." + matchedText.toLowerCase(Locale.US) + "." + idiomaticTraitName) .build(); } else { String valuePropertyPathFormatted = formatPropertyPath(instance.getTraitPropertyPath()); return validationEvent.toBuilder() .message(String.format( "'%s' trait value at path {%s} contains a non-inclusive term `%s`.%s", idiomaticTraitName, valuePropertyPathFormatted, matchedText, replacementAddendum)) .id(getName() + "." + TRAIT + "." + matchedText.toLowerCase(Locale.US) + "." + idiomaticTraitName + "." + valuePropertyPathFormatted) .build(); } case SHAPE: default: return warning(instance.getShape(), instance.getShape().getSourceLocation(), String.format("%s shape uses a non-inclusive term `%s`.%s", StringUtils.capitalize(instance.getShape().getType().toString()), matchedText, replacementAddendum), SHAPE, matchedText.toLowerCase(Locale.US)); } } private static String getReplacementAddendum(String matchedText, List replacements) { List caseCorrectedEntryValue = replacements.stream() .map(replacement -> Character.isUpperCase(matchedText.charAt(0)) ? StringUtils.capitalize(replacement) : StringUtils.uncapitalize(replacement)) .collect(Collectors.toList()); String replacementAddendum = !replacements.isEmpty() ? String.format(" Consider using one of the following terms instead: %s", ValidationUtils.tickedList(caseCorrectedEntryValue)) : ""; return replacementAddendum; } private static String formatPropertyPath(List traitPropertyPath) { return String.join("/", traitPropertyPath); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy