)
16 | undefined: null
17 | ---
18 | spring:
19 | application:
20 | # Inspection::InvalidValue(negative)
21 | name:
22 | - name1
23 | - name2
24 | server:
25 | servlet:
26 | encoding:
27 | "mapping":
28 | # Inspection::InvalidValue(negative)
29 | "en-US": "ASCII2"
30 | jetty:
31 | # Inspection::InvalidValue(negative)
32 | connection-idle-timeout: abcd
33 | logging:
34 | # Inspection::InvalidValue(negative)
35 | level: xxys
36 | logback:
37 | rolling-policy:
38 | # Inspection::InvalidValue(negative)
39 | max-file-size: 100M
40 | ---
41 | spring:
42 | # Inspection::Deprecated(negative)
43 | profiles:
44 | - abc
45 | logging:
46 | pattern:
47 | # Inspection::Deprecated(negative)
48 | rolling-file-name: "xx"
49 | server:
50 | # Inspection::Deprecated_Unsupported(negative)
51 | use-forward-headers: true
52 |
53 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/navigation/forward/AbstractReferenceProvider.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.navigation.forward;
2 |
3 | import com.intellij.psi.PsiElement;
4 | import com.intellij.psi.PsiReference;
5 | import com.intellij.psi.PsiReferenceProvider;
6 | import com.intellij.psi.PsiReferenceService;
7 | import com.intellij.util.ProcessingContext;
8 | import dev.flikas.spring.boot.assistant.idea.plugin.navigation.ReferenceService;
9 | import org.jetbrains.annotations.NotNull;
10 | import org.jetbrains.annotations.Nullable;
11 |
12 | abstract class AbstractReferenceProvider extends PsiReferenceProvider {
13 | @Override
14 | @NotNull
15 | public PsiReference @NotNull [] getReferencesByElement(
16 | @NotNull PsiElement element, @NotNull ProcessingContext context) {
17 | PsiElement source = getRefSource(element, context);
18 | if (source != null) {
19 | var svc = ReferenceService.getInstance(element.getProject());
20 | return new PsiReference[]{svc.forwardReference(source)};
21 | } else {
22 | return PsiReference.EMPTY_ARRAY;
23 | }
24 | }
25 |
26 | @Override
27 | public boolean acceptsHints(@NotNull PsiElement element, PsiReferenceService.@NotNull Hints hints) {
28 | // if (hints == PsiReferenceService.Hints.HIGHLIGHTED_REFERENCES) return false;
29 | return super.acceptsHints(element, hints);
30 | }
31 |
32 | @Nullable
33 | protected abstract PsiElement getRefSource(@NotNull PsiElement element, @NotNull ProcessingContext context);
34 | }
35 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/inspection/properties/PropertiesPropertyUsageProvider.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.inspection.properties;
2 |
3 | import com.intellij.lang.properties.codeInspection.unused.ImplicitPropertyUsageProvider;
4 | import com.intellij.lang.properties.psi.Property;
5 | import com.intellij.openapi.module.Module;
6 | import com.intellij.openapi.module.ModuleUtil;
7 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.service.ModuleMetadataService;
8 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.PropertyName;
9 | import org.apache.commons.lang3.StringUtils;
10 | import org.jetbrains.annotations.NotNull;
11 |
12 | public class PropertiesPropertyUsageProvider implements ImplicitPropertyUsageProvider {
13 | @Override
14 | public boolean isUsed(@NotNull Property property) {
15 | Module module = ModuleUtil.findModuleForPsiElement(property);
16 | if (module == null) return false;
17 | var service = ModuleMetadataService.getInstance(module);
18 | String key = property.getUnescapedKey();
19 | if (StringUtils.isBlank(key)) return false;
20 | PropertyName propertyName = PropertyName.adapt(key);
21 | if (propertyName.isLastElementIndexed()) {
22 | key = propertyName.getParent().toString();
23 | }
24 | if (service.getIndex().getProperty(key) != null) return true;
25 | var nearestParent = service.getIndex().getNearestParentProperty(key);
26 | return nearestParent != null && nearestParent.canBind(key);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/inspection/yaml/PropertyRemovedInspection.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.inspection.yaml;
2 |
3 | import com.intellij.codeInspection.ProblemsHolder;
4 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.MetadataProperty;
5 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationMetadata;
6 | import org.jetbrains.yaml.psi.YAMLKeyValue;
7 |
8 | import static dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationMetadata.Property.Deprecation.Level.ERROR;
9 |
10 | /**
11 | * Report deprecated properties whose deprecation level is error, which means that the property is completely unsupported.
12 | *
13 | * refer to Spring Boot Document
14 | */
15 | public class PropertyRemovedInspection extends PropertyDeprecatedInspectionBase {
16 | @Override
17 | protected void foundDeprecatedKey(
18 | YAMLKeyValue keyValue, MetadataProperty property,
19 | ConfigurationMetadata.Property.Deprecation deprecation, ProblemsHolder holder,
20 | boolean isOnTheFly
21 | ) {
22 | if (deprecation.getLevel() == ERROR) {
23 | assert keyValue.getKey() != null;
24 | holder.registerProblem(
25 | keyValue.getKey(),
26 | "Property \"" + property.getNameStr() + "\" is deprecated and no longer supported."
27 | );
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/index/MetadataProperty.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.index;
2 |
3 | import com.intellij.psi.PsiField;
4 | import com.intellij.psi.PsiType;
5 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationMetadata;
6 | import dev.flikas.spring.boot.assistant.idea.plugin.misc.PsiTypeUtils;
7 | import org.jetbrains.annotations.NotNull;
8 |
9 | import java.util.Optional;
10 |
11 | /**
12 | * A spring configuration metadata property
13 | */
14 | public interface MetadataProperty extends MetadataItem {
15 | /**
16 | * The PsiType include type arguments, for example, {@code Map}.
17 | *
18 | * @see ConfigurationMetadata.Property#getType()
19 | */
20 | Optional getFullType();
21 |
22 | /**
23 | * @return the field that this property will be bound to, null if not present.
24 | */
25 | Optional getSourceField();
26 |
27 | /**
28 | * get hint or value hint for this property.
29 | */
30 | Optional getHint();
31 |
32 | /**
33 | * get key hint for this property if it is a Map.
34 | */
35 | Optional getKeyHint();
36 |
37 | /**
38 | * @return whether the specified key can be bound to this property.
39 | */
40 | boolean canBind(@NotNull String key);
41 |
42 | default boolean isMapType() {
43 | return getFullType().filter(p -> PsiTypeUtils.isMap(getIndex().project(), p)).isPresent();
44 | }
45 |
46 | ConfigurationMetadata.Property getMetadata();
47 | }
48 |
--------------------------------------------------------------------------------
/plugin-test/maven/spring-boot-example/demo-app/src/main/java/dev/flikas/MyApp.java:
--------------------------------------------------------------------------------
1 | package dev.flikas;
2 |
3 | import org.springframework.beans.factory.annotation.Autowired;
4 | import org.springframework.boot.ApplicationArguments;
5 | import org.springframework.boot.ApplicationRunner;
6 | import org.springframework.boot.SpringApplication;
7 | import org.springframework.boot.autoconfigure.SpringBootApplication;
8 | import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
9 | import org.springframework.boot.context.properties.ConfigurationProperties;
10 | import org.springframework.context.annotation.Bean;
11 | import org.springframework.web.bind.annotation.GetMapping;
12 |
13 | import java.nio.charset.Charset;
14 |
15 | /**
16 | * {@link org.springframework.boot.context.config.ConfigFileApplicationListener}
17 | */
18 | @SpringBootApplication
19 | public class MyApp implements ApplicationRunner {
20 |
21 | @Autowired private MyProperties properties;
22 |
23 | public static void main(String[] args) {
24 | SpringApplication.run(MyApp.class);
25 | }
26 |
27 | @GetMapping("/hello")
28 | public String hello() {
29 | return "world";
30 | }
31 |
32 | @Bean
33 | public String bean() {
34 | return "A";
35 | }
36 |
37 | @Bean
38 | @ConfigurationProperties("example.server")
39 | public LombokPojo lombokPojo() {
40 | return new LombokPojo();
41 | }
42 |
43 | @Override
44 | public void run(ApplicationArguments args) throws Exception {
45 | System.out.println(properties.getKeyStore());
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/inspection/yaml/PropertyDeprecatedInspection.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.inspection.yaml;
2 |
3 | import com.intellij.codeInspection.ProblemsHolder;
4 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.MetadataProperty;
5 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationMetadata;
6 | import org.jetbrains.yaml.psi.YAMLKeyValue;
7 |
8 | import static dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationMetadata.Property.Deprecation.Level.WARNING;
9 |
10 | /**
11 | * Report deprecated properties whose deprecation level is warning, which means that the property is still be bound in the environment.
12 | *
13 | * refer to Spring Boot Document
14 | */
15 | public class PropertyDeprecatedInspection extends PropertyDeprecatedInspectionBase {
16 | @Override
17 | protected void foundDeprecatedKey(
18 | YAMLKeyValue keyValue, MetadataProperty property,
19 | ConfigurationMetadata.Property.Deprecation deprecation, ProblemsHolder holder,
20 | boolean isOnTheFly
21 | ) {
22 | if (deprecation.getLevel() == null || deprecation.getLevel() == WARNING) {
23 | assert keyValue.getKey() != null;
24 | holder.registerProblem(
25 | keyValue.getKey(),
26 | "Property \"" + property.getNameStr() + "\" is deprecated."
27 | );
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Available idea versions:
2 | # https://www.jetbrains.com/intellij-repository/releases
3 | # https://www.jetbrains.com/intellij-repository/snapshots
4 |
5 | vendor.name=PENG FEI
6 | vendor.email=flikas@outlook.com
7 | vendor.url=https://github.com/flikas
8 |
9 | plugin.name=Spring Boot Assistant
10 | plugin.id=dev.flikas.idea.spring.boot.assistant.plugin
11 | # MUST use semver (https://semver.org/)
12 | # The pre-release version of plugin.version will be used as JetBrains plugin release channel.
13 | # Common release channels: stable(no pre-release version),alpha,beta,eap.
14 | # https://plugins.jetbrains.com/docs/marketplace/custom-release-channels.html#configure-a-custom-channel
15 | plugin.version=601.0.0
16 |
17 | plugin.source-url=https://github.com/flikas/idea-spring-boot-assistant
18 | # This will be added to plugin's version number as 'build metadata', like '1.0.0-beta+242'.
19 | plugin.since-build=242
20 | #plugin.until-build=
21 |
22 | platform.type=IC
23 | platform.version=2024.3.3
24 | # https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html#platformVersions
25 | # 242+ -> Java 21
26 | # 222+ -> Java 17
27 | # 203+ -> Java 11
28 | platform.java-version=21
29 |
30 | # Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib
31 | kotlin.stdlib.default.dependency=false
32 | # Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html
33 | org.gradle.configuration-cache=true
34 | # Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html
35 | org.gradle.caching=true
36 |
--------------------------------------------------------------------------------
/plugin-test/maven/spring-boot-example/demo-app/src/main/resources/application-negative-cases.yaml:
--------------------------------------------------------------------------------
1 | # Inspection: Negative test cases, here should be 14 warnings ------------>
2 |
3 | # Inspection::KeyNotDefined(negative)
4 | an-undefined-key: 1
5 | # Inspection::KeyNotDefined(negative)
6 | spring:
7 | undefined: 2
8 | profiles.include:
9 | # Inspection::InvalidValue(negative), Inspection::KeyNotDefined(negative)
10 | - invalid-key: 9
11 | cloud.discovery.client.simple.instances:
12 | c:
13 | # Inspection::KeyNotDefined(negative, Map>)
14 | - undefined: xxy
15 | resilience4j.circuitbreaker.instances:
16 | "backendA":
17 | # Inspection::KeyNotDefined(negative, Map)
18 | undefined: null
19 | ---
20 | spring:
21 | application:
22 | # Inspection::InvalidValue(negative)
23 | name:
24 | - name1
25 | - name2
26 | server:
27 | servlet:
28 | encoding:
29 | "mapping":
30 | # Inspection::InvalidValue(negative)
31 | "en-US": "ASCII2"
32 | jetty:
33 | # Inspection::InvalidValue(negative)
34 | connection-idle-timeout: abcd
35 | logging:
36 | # Inspection::InvalidValue(negative)
37 | level: xxys
38 | logback:
39 | rolling-policy:
40 | # Inspection::InvalidValue(negative)
41 | max-file-size: 100M
42 | ---
43 | spring:
44 | # Inspection::Deprecated(negative)
45 | profiles:
46 | - abc
47 | logging:
48 | pattern:
49 | # Inspection::Deprecated(negative)
50 | rolling-file-name: "xx"
51 | server:
52 | # Inspection::Deprecated_Unsupported(negative)
53 | use-forward-headers: true
54 |
55 |
--------------------------------------------------------------------------------
/.github/workflows/publish-master-to-eap.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a package using Gradle and then publish it to GitHub packages when a release is created
2 | # For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#Publishing-using-gradle
3 |
4 | name: Publish to JetBrains Marketplace
5 |
6 | on:
7 | workflow_dispatch:
8 | push:
9 | tags:
10 | - '*'
11 |
12 | jobs:
13 | build:
14 | environment: JetBrains Marketplace
15 | runs-on: ubuntu-latest
16 | permissions:
17 | contents: read
18 | packages: write
19 |
20 | steps:
21 | - uses: actions/checkout@v2
22 | - name: Set up JDK 21 # This jdk is just for running Gradle, the compiler will be downloaded according to Gradle's build script.
23 | uses: actions/setup-java@v4
24 | with:
25 | java-version: '21'
26 | distribution: 'temurin'
27 | cache: gradle
28 |
29 | - name: Build with Gradle
30 | run: ./gradlew buildPlugin
31 |
32 | - name: Run Plugin Verifier
33 | run: ./gradlew verifyPlugin
34 | # The USERNAME and TOKEN need to correspond to the credentials environment variables used in
35 | # the publishing section of your build.gradle
36 | - name: Publish to JetBrains Marketplace
37 | run: ./gradlew publishPlugin
38 | env:
39 | PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
40 | PRIVATE_KEY_PASSWORD: ${{ secrets.PRIVATE_KEY_PASSWORD }}
41 | PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
42 | CERTIFICATE_CHAIN: ${{ secrets.CERTIFICATE_CHAIN }}
43 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/filetype/SpringBootConfigurationYamlFileType.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.filetype;
2 |
3 | import com.intellij.icons.AllIcons;
4 | import com.intellij.openapi.fileTypes.LanguageFileType;
5 | import dev.flikas.spring.boot.assistant.idea.plugin.misc.CompositeIconUtils;
6 | import icons.Icons;
7 | import org.jetbrains.annotations.Nls;
8 | import org.jetbrains.annotations.NonNls;
9 | import org.jetbrains.annotations.NotNull;
10 | import org.jetbrains.annotations.Nullable;
11 | import org.jetbrains.yaml.YAMLLanguage;
12 |
13 | import javax.swing.*;
14 |
15 | public class SpringBootConfigurationYamlFileType extends LanguageFileType {
16 | public static final SpringBootConfigurationYamlFileType INSTANCE = new SpringBootConfigurationYamlFileType();
17 |
18 |
19 | private SpringBootConfigurationYamlFileType() {
20 | super(YAMLLanguage.INSTANCE, true);
21 | }
22 |
23 |
24 | @Override
25 | public @NonNls @NotNull String getName() {
26 | return "spring-boot-properties-yaml";
27 | }
28 |
29 |
30 | @Override
31 | public @Nls @NotNull String getDisplayName() {
32 | return "Spring Boot Configuration YAML";
33 | }
34 |
35 |
36 | @Override
37 | public @NotNull String getDescription() {
38 | return "Spring configuration yaml file";
39 | }
40 |
41 |
42 | @Override
43 | public @NotNull String getDefaultExtension() {
44 | return "yaml";
45 | }
46 |
47 |
48 | @Override
49 | public @Nullable Icon getIcon() {
50 | return CompositeIconUtils.createWithModifier(Icons.SpringBoot, AllIcons.FileTypes.Yaml);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/plugin/src/main/resources/inspectionDescriptions/KeyNotDefined.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Reports undefined keys in Spring Boot configuration files.
4 |
5 | The definition of keys is based on
6 |
7 | Spring Boot configuration metadata
8 | .
9 |
10 |
11 | An undefined key can be fixed by either:
12 |
13 | - If it is a typo, make it correct.
14 | - If it is useless, remove it.
15 | - If it is a correct user defined key:
16 |
28 |
29 |
30 |
31 | See
32 |
33 | Spring Boot document
34 | for more information.
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/index/hint/provider/ValueProvider.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.hint.provider;
2 |
3 | import com.intellij.codeInsight.completion.CompletionParameters;
4 | import com.intellij.codeInsight.completion.PrefixMatcher;
5 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.hint.Hint;
6 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationMetadata;
7 | import org.jetbrains.annotations.NotNull;
8 | import org.jetbrains.annotations.Nullable;
9 |
10 | import java.util.Collection;
11 |
12 | /**
13 | * Spring configuration metadata hint, value providers.
14 | *
15 | * @see Spring docs
16 | */
17 | public interface ValueProvider {
18 | static ValueProvider create(ConfigurationMetadata.Hint.ValueProvider metadata) {
19 | return switch (metadata.getName()) {
20 | case ANY -> null;
21 | case CLASS_REFERENCE -> new ClassReferenceValueProvider(metadata);
22 | case HANDLE_AS -> new HandleAsValueProvider(metadata);
23 | case LOGGER_NAME -> new LoggerNameValueProvider(metadata);
24 | case SPRING_BEAN_REFERENCE -> null;
25 | case SPRING_PROFILE_NAME -> null;
26 | };
27 | }
28 |
29 | default ConfigurationMetadata.Hint.ValueProvider.Type getType() {
30 | return getMetadata().getName();
31 | }
32 |
33 | ConfigurationMetadata.Hint.ValueProvider getMetadata();
34 |
35 | Collection provideValues(
36 | @NotNull CompletionParameters completionParameters,
37 | @Nullable PrefixMatcher prefixMatcher
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/filetype/SpringBootConfigurationPropertiesFileType.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.filetype;
2 |
3 | import com.intellij.icons.AllIcons;
4 | import com.intellij.lang.properties.PropertiesLanguage;
5 | import com.intellij.openapi.fileTypes.LanguageFileType;
6 | import dev.flikas.spring.boot.assistant.idea.plugin.misc.CompositeIconUtils;
7 | import icons.Icons;
8 | import org.jetbrains.annotations.Nls;
9 | import org.jetbrains.annotations.NonNls;
10 | import org.jetbrains.annotations.NotNull;
11 | import org.jetbrains.annotations.Nullable;
12 |
13 | import javax.swing.*;
14 |
15 | public class SpringBootConfigurationPropertiesFileType extends LanguageFileType {
16 | public static final SpringBootConfigurationPropertiesFileType INSTANCE = new SpringBootConfigurationPropertiesFileType();
17 |
18 |
19 | private SpringBootConfigurationPropertiesFileType() {
20 | super(PropertiesLanguage.INSTANCE, true);
21 | }
22 |
23 |
24 | @Override
25 | public @NonNls @NotNull String getName() {
26 | return "sba-spring-boot-configuration-properties";
27 | }
28 |
29 |
30 | @Override
31 | public @Nls @NotNull String getDisplayName() {
32 | return "Spring Boot Configuration Properties";
33 | }
34 |
35 |
36 | @Override
37 | public @NotNull String getDescription() {
38 | return "Spring Boot configuration properties file";
39 | }
40 |
41 |
42 | @Override
43 | public @NotNull String getDefaultExtension() {
44 | return "properties";
45 | }
46 |
47 |
48 | @Override
49 | public @Nullable Icon getIcon() {
50 | return CompositeIconUtils.createWithModifier(Icons.SpringBoot, AllIcons.FileTypes.Properties);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/navigation/YamlToPsiReference.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.navigation;
2 |
3 | import com.intellij.openapi.util.TextRange;
4 | import com.intellij.psi.PsiElement;
5 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.PropertyName;
6 | import org.jetbrains.annotations.NotNull;
7 | import org.jetbrains.yaml.YAMLUtil;
8 | import org.jetbrains.yaml.psi.YAMLKeyValue;
9 |
10 | import java.util.Iterator;
11 |
12 | class YamlToPsiReference extends SpringPropertyToPsiReference {
13 | YamlToPsiReference(@NotNull YAMLKeyValue source) {
14 | super(source);
15 | }
16 |
17 | @Override
18 | protected Iterator candidateKeys(YAMLKeyValue key) {
19 | PropertyName fullKey = PropertyName.adapt(YAMLUtil.getConfigFullName(key));
20 | PropertyName thisKey = PropertyName.adapt(key.getKeyText());
21 | assert fullKey.toString().endsWith(thisKey.toString());
22 | PropertyName prefix = fullKey.chop(fullKey.getNumberOfElements() - thisKey.getNumberOfElements());
23 | return new Iterator<>() {
24 | private PropertyName next = thisKey;
25 |
26 | @Override
27 | public boolean hasNext() {
28 | return !next.isEmpty();
29 | }
30 |
31 | @Override
32 | public String next() {
33 | PropertyName n = next;
34 | next = next.getParent();
35 | return prefix.append(n).toString();
36 | }
37 | };
38 | }
39 |
40 |
41 | @Override
42 | protected TextRange calculateDefaultRangeInElement() {
43 | PsiElement key = getElement().getKey();
44 | if (key != null) {
45 | return key.getTextRangeInParent();
46 | }
47 | return super.calculateDefaultRangeInElement();
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/navigation/forward/YamlToCodeReferenceContributor.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.navigation.forward;
2 |
3 | import com.intellij.psi.PsiElement;
4 | import com.intellij.psi.PsiReferenceContributor;
5 | import com.intellij.psi.PsiReferenceRegistrar;
6 | import com.intellij.util.ProcessingContext;
7 | import dev.flikas.spring.boot.assistant.idea.plugin.filetype.SpringBootConfigurationYamlFileType;
8 | import org.jetbrains.annotations.NotNull;
9 | import org.jetbrains.yaml.YAMLLanguage;
10 | import org.jetbrains.yaml.psi.YAMLKeyValue;
11 |
12 | import static com.intellij.patterns.PlatformPatterns.psiElement;
13 | import static com.intellij.patterns.PsiJavaPatterns.virtualFile;
14 |
15 |
16 | /**
17 | * Provides references from Spring configuration file (application.yaml) to code.
18 | */
19 | public class YamlToCodeReferenceContributor extends PsiReferenceContributor {
20 |
21 | //TODO refactor by com.intellij.psi.search.searches.DefinitionsScopedSearch.EP and
22 | // com.intellij.psi.search.searches.ReferencesSearch.EP_NAME
23 |
24 |
25 | @Override
26 | public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) {
27 | registrar.registerReferenceProvider(
28 | psiElement(YAMLKeyValue.class)
29 | .withLanguage(YAMLLanguage.INSTANCE)
30 | .inVirtualFile(virtualFile().ofType(SpringBootConfigurationYamlFileType.INSTANCE)),
31 | new AbstractReferenceProvider() {
32 | @Override
33 | protected PsiElement getRefSource(@NotNull PsiElement element, @NotNull ProcessingContext context) {
34 | return element instanceof YAMLKeyValue yamlKeyValue ? yamlKeyValue : null;
35 | }
36 | });
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/navigation/backward/PsiToSpringPropertyReferenceSearcher.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.navigation.backward;
2 |
3 | import com.intellij.openapi.application.QueryExecutorBase;
4 | import com.intellij.openapi.project.Project;
5 | import com.intellij.psi.PsiElement;
6 | import com.intellij.psi.PsiReference;
7 | import com.intellij.psi.search.GlobalSearchScope;
8 | import com.intellij.psi.search.SearchScope;
9 | import com.intellij.psi.search.searches.ReferencesSearch;
10 | import com.intellij.util.Processor;
11 | import dev.flikas.spring.boot.assistant.idea.plugin.navigation.ReferenceService;
12 | import org.jetbrains.annotations.NotNull;
13 |
14 | public class PsiToSpringPropertyReferenceSearcher
15 | extends QueryExecutorBase {
16 | protected PsiToSpringPropertyReferenceSearcher() {
17 | super(true);
18 | }
19 |
20 |
21 | @Override
22 | public void processQuery(
23 | @NotNull ReferencesSearch.SearchParameters queryParameters, @NotNull Processor super PsiReference> consumer) {
24 | if (!queryParameters.areValid()) return;
25 | SearchScope searchScope = queryParameters.getScopeDeterminedByUser();
26 | if (!(searchScope instanceof GlobalSearchScope)) return;
27 | PsiElement element = queryParameters.getElementToSearch();
28 |
29 | Project project = element.getProject();
30 | ReferenceService service = ReferenceService.getInstance(project);
31 | service.backwardReference(element)
32 | .stream()
33 | .filter(ref -> ref.getElement().isValid())
34 | .filter(ref -> searchScope.contains(ref.getElement().getContainingFile().getVirtualFile()))
35 | .forEach(consumer::process);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/source/MetadataFileIndexConfigurator.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.source;
2 |
3 | import com.intellij.openapi.module.Module;
4 | import com.intellij.openapi.module.ModuleManager;
5 | import com.intellij.openapi.project.Project;
6 | import com.intellij.openapi.vfs.VirtualFile;
7 | import com.intellij.util.indexing.IndexableSetContributor;
8 | import dev.flikas.spring.boot.assistant.idea.plugin.misc.ModuleRootUtils;
9 | import org.jetbrains.annotations.NotNull;
10 |
11 | import java.util.HashSet;
12 | import java.util.Set;
13 |
14 | /**
15 | * Add project's {@code /META-INF/(additional-)spring-configuration-metadata.json} files into file-based index, for {@link MetadataFileIndex}.
16 | */
17 | public class MetadataFileIndexConfigurator extends IndexableSetContributor {
18 | @Override
19 | public @NotNull Set getAdditionalRootsToIndex() {
20 | return Set.of();
21 | }
22 |
23 |
24 | @Override
25 | public @NotNull Set getAdditionalProjectRootsToIndex(@NotNull Project project) {
26 | Set files = new HashSet<>();
27 | for (Module module : ModuleManager.getInstance(project).getModules()) {
28 | for (VirtualFile classRoot : ModuleRootUtils.getClassRootsWithoutLibraries(module)) {
29 | VirtualFile metaDir = classRoot.findChild(MetadataFileIndex.META_FILE_DIR);
30 | if (metaDir == null) continue;
31 | VirtualFile f = metaDir.findChild(MetadataFileIndex.METADATA_FILE_NAME);
32 | if (f != null) files.add(f);
33 | f = metaDir.findChild(MetadataFileIndex.ADDITIONAL_METADATA_FILE_NAME);
34 | if (f != null) files.add(f);
35 | }
36 | }
37 | return files;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/plugin/src/test/resources/application.yaml:
--------------------------------------------------------------------------------
1 | server:
2 | jetty:
3 | connection-idle-timeout:
4 | address:
5 | reactive:
6 | session:
7 | timeout:
8 | tomcat:
9 | accept-count:
10 | connection-timeout: 60
11 | threads:
12 | max:
13 | accesslog:
14 | buffered:
15 | basedir:
16 | port: xxf
17 | abc: x
18 | spring:
19 | devtools:
20 | add-properties:
21 | remote:
22 | application:
23 | name:
24 | cloud.discovery.client.simple:
25 | instances:
26 | a:
27 | - port: 1
28 | host: 127.0.0.1
29 | - host: 127.0.0.1
30 | serviceId: service
31 | port: 12
32 | b:
33 | - host: 127.0.0.1
34 | port: 12
35 | activemq:
36 | packages:
37 | trust-all:
38 | artemis:
39 | embedded:
40 | data-directory:
41 | logging:
42 | charset:
43 | console: utf-8
44 | pattern:
45 | rolling-file-name: "xx"
46 | level:
47 | root: info
48 | logback:
49 | rollingpolicy:
50 | max-file-size: 100MB
51 |
52 |
53 | resilience4j:
54 | circuitbreaker:
55 | instances:
56 | backendA:
57 | permittedNumberOfCallsInHalfOpenState:
58 | exponentialBackoffMultiplier:
59 | allowHealthIndicatorToFail:
60 | baseConfig: woha
61 | backendB:
62 | baseConfig: o
63 | failureRateThreshold:
64 |
65 |
66 | ---
67 | example:
68 | server:
69 | port:
70 | key: value
71 | - name: python
72 | id: 1
73 | - name: "c++"
74 | id: 2
75 | client:
76 | - client1
77 | - "client2"
78 | longString: |
79 | abcidosie
80 | sjigoes
81 | jsdfiwoeige
82 | longString2: >-
83 | jsaidfoei
84 | sdjfoiwe
85 | sjfoeiwreow
86 |
87 |
88 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/index/hint/value/ValueHint.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.hint.value;
2 |
3 | import com.intellij.icons.AllIcons;
4 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.hint.Hint;
5 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationMetadata;
6 | import org.jetbrains.annotations.NotNull;
7 | import org.jetbrains.annotations.Nullable;
8 |
9 | public class ValueHint {
10 | private final ConfigurationMetadata.Hint.ValueHint metadata;
11 |
12 |
13 | public ValueHint(ConfigurationMetadata.Hint.ValueHint metadata) {
14 | this.metadata = metadata;
15 | }
16 |
17 |
18 | /**
19 | * @see ConfigurationMetadata.Hint.ValueHint#getValue()
20 | */
21 | @NotNull
22 | public Object getValue() {
23 | return metadata.getValue();
24 | }
25 |
26 |
27 | /**
28 | * @see ConfigurationMetadata.Hint.ValueHint#getDescription()
29 | */
30 | @Nullable
31 | public String getDescription() {
32 | return metadata.getDescription();
33 | }
34 |
35 |
36 | public Hint toHint() {
37 | return new Hint(
38 | String.valueOf(getValue()),
39 | getFirstLine(getDescription()),
40 | getDescription(),
41 | AllIcons.Nodes.Field);
42 | }
43 |
44 |
45 | private String getFirstLine(@Nullable String paragraph) {
46 | if (paragraph == null) return null;
47 | int dot = paragraph.indexOf('.');
48 | int ls = paragraph.indexOf('\n');
49 | int end;
50 | if (dot > 0 && ls > 0) {
51 | end = Math.min(dot, ls);
52 | } else if (dot > 0) {
53 | end = dot;
54 | } else if (ls > 0) {
55 | end = ls;
56 | } else {
57 | return paragraph;
58 | }
59 | return paragraph.substring(0, end);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/navigation/forward/PropertiesToCodeReferenceContributor.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.navigation.forward;
2 |
3 | import com.intellij.lang.properties.PropertiesLanguage;
4 | import com.intellij.lang.properties.psi.impl.PropertyKeyImpl;
5 | import com.intellij.patterns.PlatformPatterns;
6 | import com.intellij.psi.PsiElement;
7 | import com.intellij.psi.PsiReferenceContributor;
8 | import com.intellij.psi.PsiReferenceRegistrar;
9 | import com.intellij.util.ProcessingContext;
10 | import dev.flikas.spring.boot.assistant.idea.plugin.filetype.SpringBootConfigurationPropertiesFileType;
11 | import org.jetbrains.annotations.NotNull;
12 |
13 | import static com.intellij.patterns.PsiJavaPatterns.virtualFile;
14 |
15 |
16 | /**
17 | * Provides references from Spring configuration file (application.properties) to code.
18 | */
19 | public class PropertiesToCodeReferenceContributor extends PsiReferenceContributor {
20 |
21 | //TODO refactor by com.intellij.psi.search.searches.DefinitionsScopedSearch.EP and
22 | // com.intellij.psi.search.searches.ReferencesSearch.EP_NAME
23 |
24 |
25 | @Override
26 | public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) {
27 | registrar.registerReferenceProvider(
28 | PlatformPatterns.psiElement(PropertyKeyImpl.class)
29 | .withLanguage(PropertiesLanguage.INSTANCE)
30 | .inVirtualFile(virtualFile().ofType(SpringBootConfigurationPropertiesFileType.INSTANCE)),
31 | new AbstractReferenceProvider() {
32 | @Override
33 | protected PsiElement getRefSource(@NotNull PsiElement element, @NotNull ProcessingContext context) {
34 | return element instanceof PropertyKeyImpl key ? key : null;
35 | }
36 | });
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/index/hint/Hint.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.hint;
2 |
3 | import com.intellij.psi.PsiElement;
4 | import org.jetbrains.annotations.NotNull;
5 | import org.jetbrains.annotations.Nullable;
6 |
7 | import javax.swing.*;
8 |
9 | public record Hint(
10 | @NotNull String value,
11 | @Nullable String oneLineDescription,
12 | @Nullable String description,
13 | @Nullable Icon icon,
14 | @Nullable PsiElement psiElement,
15 | @Nullable Integer priorityGroup
16 | ) implements Comparable {
17 | public Hint(@NotNull String value, @NotNull PsiElement psiElement) {
18 | this(value, null, null, null, psiElement, null);
19 | }
20 |
21 |
22 | public Hint(@NotNull String value, @NotNull PsiElement psiElement, int priorityGroup) {
23 | this(value, null, null, null, psiElement, priorityGroup);
24 | }
25 |
26 |
27 | public Hint(@NotNull String value, @NotNull Icon icon) {
28 | this(value, null, null, icon, null, null);
29 | }
30 |
31 |
32 | public Hint(@NotNull String value, @NotNull Icon icon, int priorityGroup) {
33 | this(value, null, null, icon, null, priorityGroup);
34 | }
35 |
36 |
37 | public Hint(
38 | @NotNull String value, @Nullable String oneLineDescription, @Nullable String description, @NotNull Icon icon) {
39 | this(value, oneLineDescription, description, icon, null, null);
40 | }
41 |
42 |
43 | public Hint(
44 | @NotNull String value, @Nullable String oneLineDescription, @Nullable String description, @NotNull Icon icon,
45 | int priorityGroup
46 | ) {
47 | this(value, oneLineDescription, description, icon, null, priorityGroup);
48 | }
49 |
50 |
51 | @Override
52 | public int compareTo(@NotNull Hint o) {
53 | return this.value.compareTo(o.value);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/navigation/SpringPropertyToPsiReference.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.navigation;
2 |
3 | import com.intellij.codeInspection.reference.PsiMemberReference;
4 | import com.intellij.openapi.module.Module;
5 | import com.intellij.openapi.module.ModuleUtil;
6 | import com.intellij.psi.PsiElement;
7 | import com.intellij.psi.PsiReferenceBase;
8 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.MetadataItem;
9 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.MetadataProperty;
10 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.service.ModuleMetadataService;
11 | import org.jetbrains.annotations.NotNull;
12 | import org.jetbrains.annotations.Nullable;
13 |
14 | import java.util.Iterator;
15 |
16 | abstract class SpringPropertyToPsiReference
17 | extends PsiReferenceBase implements PsiMemberReference {
18 |
19 | SpringPropertyToPsiReference(@NotNull T source) {
20 | super(source, true);
21 | }
22 |
23 | @Override
24 | public @Nullable PsiElement resolve() {
25 | Module module = ModuleUtil.findModuleForPsiElement(getElement());
26 | if (module == null) return null;
27 |
28 | Iterator candidates = candidateKeys(getElement());
29 |
30 | ModuleMetadataService metadataService = ModuleMetadataService.getInstance(module);
31 | while (candidates.hasNext()) {
32 | String key = candidates.next();
33 | MetadataItem propertyOrGroup = metadataService.getIndex().getPropertyOrGroup(key);
34 | if (propertyOrGroup == null) continue;
35 | if (propertyOrGroup instanceof MetadataProperty property) {
36 | return property.getSourceField().map(f -> (PsiElement) f).or(property::getSourceType).orElse(null);
37 | } else {
38 | return propertyOrGroup.getSourceType().orElse(null);
39 | }
40 | }
41 | return null;
42 | }
43 |
44 | protected abstract Iterator candidateKeys(T source);
45 | }
46 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/index/FileMetadataSource.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.index;
2 |
3 | import com.google.gson.Gson;
4 | import com.intellij.openapi.vfs.VirtualFile;
5 | import com.intellij.openapi.vfs.VirtualFileManager;
6 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationMetadata;
7 | import lombok.Getter;
8 |
9 | import java.io.IOException;
10 | import java.io.InputStreamReader;
11 | import java.io.Reader;
12 |
13 | @SuppressWarnings("LombokGetterMayBeUsed")
14 | public class FileMetadataSource extends AbstractMetadataSource {
15 | private static final ThreadLocal gson = ThreadLocal.withInitial(Gson::new);
16 | @Getter private VirtualFile source;
17 |
18 |
19 | public FileMetadataSource(VirtualFile source) {
20 | this.source = source;
21 | }
22 |
23 |
24 | /**
25 | * If current source {@link VirtualFile} is invalid, try to recreate it with the same URL,
26 | * if succeeded, return true.
27 | *
28 | * @return true if current source file is invalid and the recreation is succeeded.
29 | */
30 | public boolean tryReloadIfInvalid() {
31 | if (!isValid()) {
32 | VirtualFile vf = VirtualFileManager.getInstance().refreshAndFindFileByUrl(this.source.getUrl());
33 | if (vf != null) {
34 | this.source = vf;
35 | return true;
36 | }
37 | }
38 | return false;
39 | }
40 |
41 |
42 | @Override
43 | public boolean isValid() {
44 | return source.isValid();
45 | }
46 |
47 |
48 | @Override
49 | public String getPresentation() {
50 | return source.toString();
51 | }
52 |
53 |
54 | public ConfigurationMetadata getContent() throws IOException {
55 | try (Reader reader = new InputStreamReader(source.getInputStream(), source.getCharset())) {
56 | ConfigurationMetadata metadata = gson.get().fromJson(reader, ConfigurationMetadata.class);
57 | markSynchronized();
58 | return metadata;
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/index/MetadataHintImpl.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.index;
2 |
3 | import com.intellij.openapi.diagnostic.Logger;
4 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.hint.provider.HandleAsValueProvider;
5 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.hint.provider.ValueProvider;
6 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.hint.value.ValueHint;
7 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationMetadata;
8 | import lombok.EqualsAndHashCode;
9 | import lombok.Getter;
10 | import lombok.ToString;
11 |
12 | import java.util.List;
13 | import java.util.Objects;
14 | import java.util.stream.Stream;
15 |
16 | @EqualsAndHashCode(of = "metadata")
17 | @ToString(of = "metadata")
18 | @Getter
19 | class MetadataHintImpl implements MetadataHint {
20 | private static final Logger LOG = Logger.getInstance(HandleAsValueProvider.class);
21 | private final ConfigurationMetadata.Hint metadata;
22 | private final List values;
23 | private final List providers;
24 |
25 |
26 | public MetadataHintImpl(ConfigurationMetadata.Hint metadata) {
27 | this.metadata = metadata;
28 |
29 | ConfigurationMetadata.Hint.ValueHint[] values = metadata.getValues();
30 | if (values == null) {
31 | this.values = List.of();
32 | } else {
33 | this.values = Stream.of(values).map(ValueHint::new).toList();
34 | }
35 |
36 | ConfigurationMetadata.Hint.ValueProvider[] providers = metadata.getProviders();
37 | if (providers == null) {
38 | this.providers = List.of();
39 | } else {
40 | this.providers = Stream.of(providers).map(m -> {
41 | try {
42 | return ValueProvider.create(m);
43 | } catch (Exception e) {
44 | LOG.warn("Invalid hint configuration, ignored: " + metadata.getName(), e);
45 | return null;
46 | }
47 | }).filter(Objects::nonNull).toList();
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/inspection/yaml/YamlInspectionBase.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.inspection.yaml;
2 |
3 | import com.intellij.codeInspection.LocalInspectionTool;
4 | import com.intellij.codeInspection.LocalInspectionToolSession;
5 | import com.intellij.codeInspection.ProblemsHolder;
6 | import com.intellij.openapi.fileTypes.FileTypeManager;
7 | import com.intellij.openapi.module.Module;
8 | import com.intellij.openapi.module.ModuleUtil;
9 | import com.intellij.openapi.progress.ProgressIndicatorProvider;
10 | import com.intellij.openapi.vfs.VirtualFile;
11 | import com.intellij.psi.PsiElementVisitor;
12 | import com.intellij.psi.PsiFile;
13 | import dev.flikas.spring.boot.assistant.idea.plugin.filetype.SpringBootConfigurationYamlFileType;
14 | import org.jetbrains.annotations.NotNull;
15 | import org.jetbrains.yaml.psi.YAMLKeyValue;
16 | import org.jetbrains.yaml.psi.YamlPsiElementVisitor;
17 |
18 | public abstract class YamlInspectionBase extends LocalInspectionTool {
19 | @Override
20 | public boolean isAvailableForFile(@NotNull PsiFile file) {
21 | VirtualFile virtualFile = file.getVirtualFile();
22 | if (virtualFile == null) {
23 | return false;
24 | }
25 | FileTypeManager ftm = FileTypeManager.getInstance();
26 | return ftm.isFileOfType(virtualFile, SpringBootConfigurationYamlFileType.INSTANCE);
27 | }
28 |
29 |
30 | @Override
31 | public @NotNull PsiElementVisitor buildVisitor(
32 | @NotNull ProblemsHolder holder, boolean isOnTheFly, @NotNull LocalInspectionToolSession session
33 | ) {
34 | Module module = ModuleUtil.findModuleForFile(session.getFile());
35 | if (module == null) return PsiElementVisitor.EMPTY_VISITOR;
36 |
37 | return new YamlPsiElementVisitor() {
38 | @Override
39 | public void visitKeyValue(@NotNull YAMLKeyValue keyValue) {
40 | ProgressIndicatorProvider.checkCanceled();
41 | YamlInspectionBase.this.visitKeyValue(module, keyValue, holder, isOnTheFly);
42 | }
43 | };
44 | }
45 |
46 |
47 | protected abstract void visitKeyValue(
48 | @NotNull Module module, @NotNull YAMLKeyValue keyValue, @NotNull ProblemsHolder holder, boolean isOnTheFly
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/plugin-test/maven/spring-boot-example/demo-app/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | spring-boot-example
7 | org.example
8 | 1.0-SNAPSHOT
9 |
10 | 4.0.0
11 |
12 | demo-app
13 |
14 |
15 |
16 | org.springframework.boot
17 | spring-boot-starter-webflux
18 |
19 |
20 | org.springframework.cloud
21 | spring-cloud-starter-circuitbreaker-resilience4j
22 |
23 |
24 | org.springframework.boot
25 | spring-boot-starter-data-jpa
26 |
27 |
28 | com.h2database
29 | h2
30 |
31 |
32 | com.mysql
33 | mysql-connector-j
34 |
35 |
36 | com.oracle.database.jdbc
37 | ojdbc11
38 |
39 |
40 | org.springframework.boot
41 | spring-boot-starter-batch
42 |
43 |
44 | org.springframework.boot
45 | spring-boot-configuration-processor
46 |
47 |
48 | org.springframework.boot
49 | spring-boot-devtools
50 |
51 |
52 | org.springframework.session
53 | spring-session-data-redis
54 |
55 |
56 | org.example
57 | demo-lib
58 | 1.0-SNAPSHOT
59 |
60 |
61 |
--------------------------------------------------------------------------------
/plugin-test/maven/multi-module/child1/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 |
7 | com.acme
8 | multi-module
9 | 0.0.1-SNAPSHOT
10 |
11 |
12 | child1
13 | child1
14 | Child1
15 |
16 |
17 | UTF-8
18 | UTF-8
19 | 1.8
20 | Finchley.M4
21 |
22 |
23 |
24 |
25 | org.springframework.boot
26 | spring-boot-test-autoconfigure
27 | test
28 |
29 |
30 |
31 |
32 |
33 |
34 | org.springframework.boot
35 | spring-boot-maven-plugin
36 |
37 |
38 |
39 |
40 |
41 |
42 | spring-snapshots
43 | Spring Snapshots
44 | https://repo.spring.io/snapshot
45 |
46 | true
47 |
48 |
49 |
50 | spring-milestones
51 | Spring Milestones
52 | https://repo.spring.io/milestone
53 |
54 | false
55 |
56 |
57 |
58 |
59 |
60 |
61 | spring-snapshots
62 | Spring Snapshots
63 | https://repo.spring.io/snapshot
64 |
65 | true
66 |
67 |
68 |
69 | spring-milestones
70 | Spring Milestones
71 | https://repo.spring.io/milestone
72 |
73 | false
74 |
75 |
76 |
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/index/HomonymProperties.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.index;
2 |
3 | import com.intellij.openapi.diagnostic.Logger;
4 | import lombok.experimental.Delegate;
5 | import org.jetbrains.annotations.NotNull;
6 |
7 | import java.util.AbstractMap;
8 | import java.util.Set;
9 | import java.util.concurrent.ConcurrentHashMap;
10 | import java.util.concurrent.ConcurrentMap;
11 |
12 | /**
13 | * Aggregate properties which have the same name.
14 | */
15 | public class HomonymProperties extends AbstractMap implements MetadataProperty {
16 | private static final Logger LOG = Logger.getInstance(HomonymProperties.class);
17 |
18 | private final ConcurrentMap items = new ConcurrentHashMap<>();
19 | @Delegate
20 | private MetadataProperty mainProperty;
21 |
22 |
23 | public HomonymProperties(String source, MetadataProperty property) {
24 | add(source, property);
25 | }
26 |
27 |
28 | @Override
29 | public @NotNull Set> entrySet() {
30 | return items.entrySet();
31 | }
32 |
33 |
34 | public void add(@NotNull String source, MetadataProperty property) {
35 | MetadataProperty current = items.putIfAbsent(property.getNameStr(), property);
36 | if (current != null && !current.equals(property)) {
37 | LOG.warn("Duplicate property " + property.getNameStr() + " in " + source + ", ignored");
38 | } else if (current == null) {
39 | if (this.mainProperty == null) {
40 | this.mainProperty = property;
41 | } else if (this.mainProperty.getMetadata().getDeprecation() != null
42 | && property.getMetadata().getDeprecation() == null) {
43 | this.mainProperty = property;
44 | } else if (property.getMetadata().getDeprecation() == null) {
45 | LOG.warn("Duplicate property '" + property.getNameStr() + "' & '"
46 | + this.mainProperty.getNameStr() + "' in " + source + ", ignored");
47 | }
48 | }
49 | }
50 |
51 |
52 | public void addAll(String source, Iterable properties) {
53 | for (MetadataProperty property : properties) {
54 | add(source, property);
55 | }
56 | }
57 |
58 |
59 | public HomonymProperties merge(String source, HomonymProperties properties) {
60 | addAll(source, properties.values());
61 | return this;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/inspection/yaml/PropertyDeprecatedInspectionBase.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.inspection.yaml;
2 |
3 | import com.intellij.codeInspection.ProblemsHolder;
4 | import com.intellij.openapi.module.Module;
5 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.MetadataProperty;
6 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.service.ModuleMetadataService;
7 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationMetadata.Property.Deprecation;
8 | import org.jetbrains.annotations.NotNull;
9 | import org.jetbrains.yaml.YAMLUtil;
10 | import org.jetbrains.yaml.psi.YAMLAlias;
11 | import org.jetbrains.yaml.psi.YAMLKeyValue;
12 | import org.jetbrains.yaml.psi.YAMLMapping;
13 | import org.jetbrains.yaml.psi.YAMLValue;
14 |
15 | public abstract class PropertyDeprecatedInspectionBase extends YamlInspectionBase {
16 | @Override
17 | protected void visitKeyValue(
18 | @NotNull Module module, @NotNull YAMLKeyValue keyValue, @NotNull ProblemsHolder holder, boolean isOnTheFly
19 | ) {
20 | YAMLValue yamlValue = keyValue.getValue();
21 | if (yamlValue == null) return;
22 | if (yamlValue instanceof YAMLAlias) return; //TODO Support YAML alias.
23 |
24 | String propertyName = YAMLUtil.getConfigFullName(keyValue);
25 | ModuleMetadataService service = module.getService(ModuleMetadataService.class);
26 | MetadataProperty property = service.getIndex().getProperty(propertyName);
27 | if (property == null) return;
28 |
29 | if (yamlValue instanceof YAMLMapping && !property.isMapType()) {
30 | // Property isValid, its value in YAML is a mapping, but the property's type is not a Map: this may happen on
31 | // property deprecation, for example, "spring.profiles" & "spring.profiles.active/group/include/...".
32 | // If it happens, we should only prompt deprecation while the actual value type coincides with the property's type.
33 | return;
34 | }
35 | Deprecation deprecation = property.getMetadata().getDeprecation();
36 | if (deprecation != null) {
37 | foundDeprecatedKey(keyValue, property, deprecation, holder, isOnTheFly);
38 | }
39 | }
40 |
41 |
42 | protected abstract void foundDeprecatedKey(
43 | YAMLKeyValue keyValue, MetadataProperty property, Deprecation deprecation,
44 | ProblemsHolder holder, boolean isOnTheFly
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/service/ProjectMetadataService.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.service;
2 |
3 | import com.intellij.openapi.Disposable;
4 | import com.intellij.openapi.components.Service;
5 | import com.intellij.openapi.project.Project;
6 | import com.intellij.openapi.roots.ModuleRootModel;
7 | import com.intellij.openapi.vfs.VirtualFile;
8 | import com.intellij.task.ProjectTaskListener;
9 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.MetadataIndex;
10 | import dev.flikas.spring.boot.assistant.idea.plugin.misc.MutableReference;
11 | import lombok.Getter;
12 | import org.jetbrains.annotations.NotNull;
13 |
14 | import java.util.concurrent.ConcurrentHashMap;
15 | import java.util.concurrent.ConcurrentMap;
16 |
17 | import static com.intellij.openapi.compiler.CompilerTopics.COMPILATION_STATUS;
18 |
19 | /**
20 | * Service that generates {@link MetadataIndex} from one {@linkplain ModuleRootModel#getSourceRoots() SourceRoot}.
21 | *
22 | * It searches and generate index from Spring Configuration Files
23 | * in the source root and watches them for automatically update the index.
24 | */
25 | @Service(Service.Level.PROJECT)
26 | final class ProjectMetadataService implements Disposable {
27 | private final Project project;
28 | private final ConcurrentMap metadataFiles = new ConcurrentHashMap<>();
29 | @Getter private final MetadataIndex emptyIndex;
30 |
31 |
32 | public ProjectMetadataService(Project project) {
33 | this.project = project;
34 | this.emptyIndex = MetadataIndex.empty(this.project);
35 | CompilationListener compilationListener = new CompilationListener(project);
36 | project.getMessageBus().connect().subscribe(COMPILATION_STATUS, compilationListener);
37 | // For gradle delegated build
38 | project.getMessageBus().connect().subscribe(ProjectTaskListener.TOPIC, compilationListener);
39 | }
40 |
41 |
42 | public MutableReference getIndexForMetaFile(@NotNull VirtualFile metadataFile) {
43 | return getIndex(metadataFile);
44 | }
45 |
46 |
47 | @Override
48 | public void dispose() {
49 | // This is a parent disposable for FileWatcher.
50 | }
51 |
52 |
53 | private MetadataFileContainer getIndex(@NotNull VirtualFile metadataFile) {
54 | return metadataFiles.computeIfAbsent(metadataFile.getUrl(),
55 | url -> new MetadataFileContainer(metadataFile, project));
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/completion/properties/PropertiesValueInsertHandler.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.completion.properties;
2 |
3 | import com.intellij.codeInsight.completion.InsertHandler;
4 | import com.intellij.codeInsight.completion.InsertionContext;
5 | import com.intellij.codeInsight.lookup.LookupElement;
6 | import com.intellij.lang.properties.psi.PropertiesResourceBundleUtil;
7 | import com.intellij.lang.properties.psi.Property;
8 | import com.intellij.lang.properties.psi.PropertyKeyValueFormat;
9 | import com.intellij.lang.properties.psi.codeStyle.PropertiesCodeStyleSettings;
10 | import com.intellij.openapi.project.Project;
11 | import com.intellij.psi.PsiElement;
12 | import com.intellij.psi.util.PsiTreeUtil;
13 | import net.jcip.annotations.ThreadSafe;
14 | import org.jetbrains.annotations.NotNull;
15 |
16 | @ThreadSafe
17 | class PropertiesValueInsertHandler implements InsertHandler {
18 | public static final PropertiesValueInsertHandler INSTANCE = new PropertiesValueInsertHandler();
19 |
20 |
21 | private PropertiesValueInsertHandler() {}
22 |
23 |
24 | @Override
25 | public void handleInsert(@NotNull InsertionContext context, @NotNull LookupElement lookupElement) {
26 | Project project = context.getProject();
27 | PsiElement currentElement = context.getFile().findElementAt(context.getStartOffset());
28 | assert currentElement != null : "no element at " + context.getStartOffset();
29 | Property property = PsiTreeUtil.getParentOfType(currentElement, Property.class);
30 | if (property == null) return;
31 |
32 | String escaped = escapeValue(project, lookupElement.getLookupString());
33 | if (!escaped.equals(lookupElement.getLookupString())) {
34 | property.setValue(lookupElement.getLookupString(), PropertyKeyValueFormat.MEMORY);
35 | }
36 |
37 | //TODO Add '\n' if the value is 'complete'(for example, 'classpath:' is not complete.
38 | // if (context.getCompletionChar() == '\n') {
39 | // Editor editor = context.getEditor();
40 | // editor.getCaretModel().moveToOffset(property.getTextOffset() + property.getTextLength());
41 | // EditorModificationUtil.insertStringAtCaret(editor, "\n", false, true);
42 | // }
43 | }
44 |
45 |
46 | private String escapeValue(Project project, String value) {
47 | char delimiter = PropertiesCodeStyleSettings.getInstance(project).getDelimiter();
48 | return PropertiesResourceBundleUtil.convertValueToFileFormat(value, delimiter, PropertyKeyValueFormat.MEMORY);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/navigation/SpringPropertyReadWriteAccessDetector.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.navigation;
2 |
3 | import com.intellij.codeInsight.highlighting.JavaReadWriteAccessDetector;
4 | import com.intellij.lang.properties.parsing.PropertiesTokenTypes;
5 | import com.intellij.lang.properties.psi.Property;
6 | import com.intellij.openapi.fileTypes.FileTypeManager;
7 | import com.intellij.openapi.vfs.VirtualFile;
8 | import com.intellij.psi.PsiElement;
9 | import com.intellij.psi.PsiField;
10 | import com.intellij.psi.PsiFile;
11 | import com.intellij.psi.PsiReference;
12 | import dev.flikas.spring.boot.assistant.idea.plugin.filetype.SpringBootConfigurationPropertiesFileType;
13 | import dev.flikas.spring.boot.assistant.idea.plugin.filetype.SpringBootConfigurationYamlFileType;
14 | import org.jetbrains.annotations.NotNull;
15 | import org.jetbrains.yaml.psi.YAMLKeyValue;
16 |
17 | import java.util.Optional;
18 |
19 | /**
20 | * This detector have to extends {@link JavaReadWriteAccessDetector},
21 | * because KotlinReadWriteAccessDetector decorate it.
22 | */
23 | public class SpringPropertyReadWriteAccessDetector extends JavaReadWriteAccessDetector {
24 | @Override
25 | public boolean isReadWriteAccessible(@NotNull PsiElement element) {
26 | return element instanceof PsiField || super.isReadWriteAccessible(element);
27 | }
28 |
29 | @Override
30 | public @NotNull Access getReferenceAccess(@NotNull PsiElement referencedElement, @NotNull PsiReference reference) {
31 | return reference instanceof SpringPropertyToPsiReference
32 | ? Access.Write
33 | : super.getReferenceAccess(referencedElement, reference);
34 | }
35 |
36 | @Override
37 | public @NotNull Access getExpressionAccess(@NotNull PsiElement expression) {
38 | return isSpringProperty(expression)
39 | ? Access.Write
40 | : super.getExpressionAccess(expression);
41 | }
42 |
43 | private boolean isSpringProperty(PsiElement element) {
44 | VirtualFile vf = Optional.ofNullable(element.getContainingFile()).map(PsiFile::getVirtualFile).orElse(null);
45 | if (vf == null) return false;
46 | var vfm = FileTypeManager.getInstance();
47 | return (vfm.isFileOfType(vf, SpringBootConfigurationYamlFileType.INSTANCE) && element instanceof YAMLKeyValue)
48 | || (vfm.isFileOfType(vf, SpringBootConfigurationPropertiesFileType.INSTANCE)
49 | && (element instanceof Property
50 | || element.getNode().getElementType() == PropertiesTokenTypes.KEY_CHARACTERS));
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/plugin-test/gradle/multi-module/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/plugin-test/gradle/single-project/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/completion/SourceContainer.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.completion;
2 |
3 | import com.intellij.lang.Language;
4 | import com.intellij.navigation.ItemPresentation;
5 | import com.intellij.openapi.project.Project;
6 | import com.intellij.openapi.util.NlsSafe;
7 | import com.intellij.psi.PsiManager;
8 | import com.intellij.psi.impl.light.LightElement;
9 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.MetadataItem;
10 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.hint.Hint;
11 | import kotlin.Pair;
12 | import lombok.ToString;
13 | import org.jetbrains.annotations.NotNull;
14 | import org.jetbrains.annotations.Nullable;
15 |
16 | import javax.swing.*;
17 | import java.util.Optional;
18 |
19 | /**
20 | * A PsiElement that carrying a source object.
21 | *
22 | * Created by Completion for each LookupElement, useful for InsertHandler, Documentation, etc.
23 | */
24 | @ToString(of = "source")
25 | public class SourceContainer extends LightElement {
26 | private final Object source;
27 |
28 |
29 | SourceContainer(@NotNull MetadataItem metadata, @NotNull Project project) {
30 | this(metadata, PsiManager.getInstance(project));
31 | }
32 |
33 |
34 | SourceContainer(@NotNull Hint metadata, @NotNull Project project) {
35 | this(metadata, PsiManager.getInstance(project));
36 | }
37 |
38 |
39 | private SourceContainer(@NotNull Object metadata, @NotNull PsiManager psiManager) {
40 | super(psiManager, Language.ANY);
41 | this.source = metadata;
42 | assert metadata instanceof MetadataItem || metadata instanceof Hint;
43 | }
44 |
45 |
46 | @Override
47 | public ItemPresentation getPresentation() {
48 | return new ItemPresentation() {
49 | @Nullable
50 | @Override
51 | public @NlsSafe String getPresentableText() {
52 | return getSourceMetadataItem().map(MetadataItem::getNameStr).orElseGet(() ->
53 | getSourceHint().map(Hint::value).orElse(""));
54 | }
55 |
56 |
57 | @Override
58 | public @Nullable Icon getIcon(boolean unused) {
59 | return getSourceMetadataItem().map(MetadataItem::getIcon).map(Pair::getSecond).orElseGet(() ->
60 | getSourceHint().map(Hint::icon).orElse(null));
61 | }
62 | };
63 | }
64 |
65 |
66 | public Optional getSourceMetadataItem() {
67 | return source instanceof MetadataItem mi ? Optional.of(mi) : Optional.empty();
68 | }
69 |
70 |
71 | public Optional getSourceHint() {
72 | return source instanceof Hint h ? Optional.of(h) : Optional.empty();
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/inspection/yaml/KeyNotDefinedInspection.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.inspection.yaml;
2 |
3 | import com.intellij.codeInspection.ProblemsHolder;
4 | import com.intellij.openapi.module.Module;
5 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.MetadataProperty;
6 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.NameTreeNode;
7 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.service.ModuleMetadataService;
8 | import org.apache.commons.lang3.StringUtils;
9 | import org.jetbrains.annotations.NotNull;
10 | import org.jetbrains.annotations.Nullable;
11 | import org.jetbrains.yaml.YAMLBundle;
12 | import org.jetbrains.yaml.YAMLUtil;
13 | import org.jetbrains.yaml.psi.YAMLAlias;
14 | import org.jetbrains.yaml.psi.YAMLKeyValue;
15 | import org.jetbrains.yaml.psi.YAMLMapping;
16 | import org.jetbrains.yaml.psi.YAMLScalar;
17 | import org.jetbrains.yaml.psi.YAMLSequence;
18 | import org.jetbrains.yaml.psi.YAMLValue;
19 |
20 |
21 | public class KeyNotDefinedInspection extends YamlInspectionBase {
22 | @Override
23 | protected void visitKeyValue(
24 | @NotNull Module module, @NotNull YAMLKeyValue keyValue, @NotNull ProblemsHolder holder, boolean isOnTheFly
25 | ) {
26 | if (keyValue.getKey() == null) return;
27 | ModuleMetadataService service = module.getService(ModuleMetadataService.class);
28 | String fullName = YAMLUtil.getConfigFullName(keyValue);
29 | if (StringUtils.isBlank(fullName)) return;
30 | YAMLValue value = keyValue.getValue();
31 | if (value instanceof YAMLScalar || value instanceof YAMLSequence || value == null) {
32 | MetadataProperty property = service.getIndex().getProperty(fullName);
33 | if (property != null) return;
34 | } else if (value instanceof YAMLMapping) {
35 | NameTreeNode tn = service.getIndex().findInNameTrie(fullName);
36 | if (tn != null) return;
37 | } else if (value instanceof YAMLAlias) {
38 | return; //We do not support alias for now
39 | }
40 | // Property is not defined, but maybe its parent has a Map or Properties type.
41 | @Nullable MetadataProperty property = service.getIndex().getNearestParentProperty(fullName);
42 | if (property == null || !property.canBind(fullName)) {
43 | registerProblem(keyValue, holder);
44 | }
45 | }
46 |
47 |
48 | private static void registerProblem(@NotNull YAMLKeyValue keyValue, @NotNull ProblemsHolder holder) {
49 | holder.registerProblem(
50 | keyValue.getKey(),
51 | YAMLBundle.message("YamlUnknownKeysInspectionBase.unknown.key", keyValue.getKeyText())
52 | );
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/plugin-test/maven/multi-module/child2/src/main/resources/META-INF/additional-spring-configuration-metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "hints": [
3 | {
4 | "name": "custom.choice.nodefault",
5 | "values": [
6 | {
7 | "value": "a",
8 | "description": "custom a"
9 | },
10 | {
11 | "value": "b"
12 | }
13 | ]
14 | },
15 | {
16 | "name": "custom.choice.with.default",
17 | "values": [
18 | {
19 | "value": "a",
20 | "description": "custom a"
21 | },
22 | {
23 | "value": "b"
24 | }
25 | ]
26 | }
27 | ],
28 | "groups": [
29 | {
30 | "sourceType": "com.acme.Custom",
31 | "name": "custom",
32 | "type": "com.acme.Custom",
33 | "description": "Custom group long long long long long long description"
34 | }
35 | ],
36 | "properties": [
37 | {
38 | "deprecated": true,
39 | "name": "custom.deprecated.warning",
40 | "type": "java.lang.String",
41 | "deprecation": {
42 | "level": "warning",
43 | "replacement": "custom.replacement"
44 | }
45 | },
46 | {
47 | "deprecated": true,
48 | "name": "custom.deprecated.autowarning",
49 | "type": "java.lang.String",
50 | "deprecation": {
51 | "replacement": "custom.replacement"
52 | }
53 | },
54 | {
55 | "deprecated": true,
56 | "name": "custom.deprecated.error",
57 | "type": "com.acme.CustomProperty",
58 | "description": "Should never be shown to anyone",
59 | "deprecation": {
60 | "level": "error"
61 | }
62 | },
63 | {
64 | "defaultValue": "fresh",
65 | "name": "custom.replacement",
66 | "description": "I'm new, why dont you use me instead of deprecated property",
67 | "type": "java.lang.String"
68 | },
69 | {
70 | "defaultValue": "defaultvalue",
71 | "name": "custom.choice.with.default",
72 | "description": "Custom choice with default",
73 | "type": "java.lang.String"
74 | },
75 | {
76 | "name": "custom.choice.nodefault",
77 | "description": "No default present",
78 | "type": "java.lang.String"
79 | },
80 | {
81 | "name": "custom.choice.values.from.default",
82 | "description": "With choices",
83 | "type": "java.lang.String",
84 | "defaultValue": [
85 | "val1",
86 | "val2"
87 | ]
88 | },
89 | {
90 | "name": "newgroup.property.withdescription",
91 | "description": "I have description",
92 | "type": "java.lang.String"
93 | },
94 | {
95 | "name": "newgroup.property.nodesription",
96 | "type": "java.lang.Boolean"
97 | }
98 | ]
99 | }
100 |
--------------------------------------------------------------------------------
/plugin-test/gradle/multi-module/module2-additional-metadata/src/main/resources/META-INF/additional-spring-configuration-metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "hints": [
3 | {
4 | "name": "custom.choice.nodefault",
5 | "values": [
6 | {
7 | "value": "a",
8 | "description": "custom a"
9 | },
10 | {
11 | "value": "b"
12 | }
13 | ]
14 | },
15 | {
16 | "name": "custom.choice.with.default",
17 | "values": [
18 | {
19 | "value": "a",
20 | "description": "custom a"
21 | },
22 | {
23 | "value": "b"
24 | }
25 | ]
26 | }
27 | ],
28 | "groups": [
29 | {
30 | "sourceType": "com.acme.Custom",
31 | "name": "custom",
32 | "type": "com.acme.Custom",
33 | "description": "Custom group long long long long long long description"
34 | }
35 | ],
36 | "properties": [
37 | {
38 | "deprecated": true,
39 | "name": "custom.deprecated.warning",
40 | "type": "java.lang.String",
41 | "deprecation": {
42 | "level": "warning",
43 | "replacement": "custom.replacement"
44 | }
45 | },
46 | {
47 | "deprecated": true,
48 | "name": "custom.deprecated.autowarning",
49 | "type": "java.lang.String",
50 | "deprecation": {
51 | "replacement": "custom.replacement"
52 | }
53 | },
54 | {
55 | "deprecated": true,
56 | "name": "custom.deprecated.error",
57 | "type": "com.acme.CustomProperty",
58 | "description": "Should never be shown to anyone",
59 | "deprecation": {
60 | "level": "error"
61 | }
62 | },
63 | {
64 | "defaultValue": "fresh",
65 | "name": "custom.replacement",
66 | "description": "I'm new, why dont you use me instead of deprecated property",
67 | "type": "java.lang.String"
68 | },
69 | {
70 | "defaultValue": "defaultvalue",
71 | "name": "custom.choice.with.default",
72 | "description": "Custom choice with default",
73 | "type": "java.lang.String"
74 | },
75 | {
76 | "name": "custom.choice.nodefault",
77 | "description": "No default present",
78 | "type": "java.lang.String"
79 | },
80 | {
81 | "name": "custom.choice.values.from.default",
82 | "description": "With choices",
83 | "type": "java.lang.String",
84 | "defaultValue": [
85 | "val1",
86 | "val2"
87 | ]
88 | },
89 | {
90 | "name": "newgroup.property.withdescription",
91 | "description": "I have description",
92 | "type": "java.lang.String"
93 | },
94 | {
95 | "name": "newgroup.property.nodesription",
96 | "type": "java.lang.Boolean"
97 | }
98 | ]
99 | }
100 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/documentation/YamlDocumentationProvider.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.documentation;
2 |
3 | import com.intellij.lang.documentation.AbstractDocumentationProvider;
4 | import com.intellij.openapi.module.Module;
5 | import com.intellij.psi.PsiElement;
6 | import com.intellij.psi.util.PsiTreeUtil;
7 | import dev.flikas.spring.boot.assistant.idea.plugin.completion.SourceContainer;
8 | import dev.flikas.spring.boot.assistant.idea.plugin.documentation.service.DocumentationService;
9 | import dev.flikas.spring.boot.assistant.idea.plugin.filetype.SpringBootConfigurationYamlFileType;
10 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.MetadataItem;
11 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.service.ModuleMetadataService;
12 | import dev.flikas.spring.boot.assistant.idea.plugin.misc.PsiElementUtils;
13 | import org.jetbrains.annotations.Nls;
14 | import org.jetbrains.annotations.NotNull;
15 | import org.jetbrains.annotations.Nullable;
16 | import org.jetbrains.yaml.YAMLUtil;
17 | import org.jetbrains.yaml.psi.YAMLKeyValue;
18 |
19 | import static com.intellij.openapi.module.ModuleUtilCore.findModuleForPsiElement;
20 |
21 | public class YamlDocumentationProvider extends AbstractDocumentationProvider {
22 | @Nullable
23 | @Nls
24 | @Override
25 | public String generateDoc(PsiElement element, @Nullable PsiElement originalElement) {
26 | if (element instanceof SourceContainer ce) {
27 | return generateDocumentHtml(ce);
28 | }
29 |
30 | if (originalElement != null) element = originalElement;
31 | if (!PsiElementUtils.isInFileOfType(element, SpringBootConfigurationYamlFileType.INSTANCE)) {
32 | return null;
33 | }
34 | Module module = findModuleForPsiElement(element);
35 | if (module == null) {
36 | return null;
37 | }
38 | // Find context YAMLKeyValue, stop if context is not at the same line.
39 | YAMLKeyValue keyValue = PsiTreeUtil.getContextOfType(element, false, YAMLKeyValue.class);
40 | if (keyValue == null) return null;
41 | if (!YAMLUtil.psiAreAtTheSameLine(element, keyValue)) return null;
42 |
43 | String propertyName = YAMLUtil.getConfigFullName(keyValue);
44 | ModuleMetadataService service = module.getService(ModuleMetadataService.class);
45 | @Nullable MetadataItem propertyOrGroup = service.getIndex().getPropertyOrGroup(propertyName);
46 | if (propertyOrGroup == null) return null;
47 |
48 | return DocumentationService.getInstance(module.getProject()).generateDoc(propertyOrGroup);
49 | }
50 |
51 |
52 | @NotNull
53 | private String generateDocumentHtml(SourceContainer sc) {
54 | DocumentationService docSvc = DocumentationService.getInstance(sc.getProject());
55 | return sc.getSourceMetadataItem().map(docSvc::generateDoc).orElseGet(() ->
56 | sc.getSourceHint().map(docSvc::generateDoc).orElseThrow());
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/index/hint/provider/AbstractValueProvider.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.hint.provider;
2 |
3 | import com.intellij.codeInsight.completion.CompletionParameters;
4 | import com.intellij.codeInsight.completion.PrefixMatcher;
5 | import com.intellij.openapi.module.Module;
6 | import com.intellij.openapi.module.ModuleUtilCore;
7 | import com.intellij.openapi.project.Project;
8 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationMetadata;
9 | import org.jetbrains.annotations.Contract;
10 | import org.jetbrains.annotations.NotNull;
11 | import org.jetbrains.annotations.Nullable;
12 | import org.springframework.boot.convert.ApplicationConversionService;
13 | import org.springframework.core.convert.ConversionService;
14 |
15 | import java.util.Map;
16 | import java.util.Objects;
17 |
18 | abstract class AbstractValueProvider implements ValueProvider {
19 | private static final ConversionService conversionService = ApplicationConversionService.getSharedInstance();
20 | private final ConfigurationMetadata.Hint.ValueProvider metadata;
21 |
22 |
23 | AbstractValueProvider(ConfigurationMetadata.Hint.ValueProvider metadata) {
24 | this.metadata = metadata;
25 | }
26 |
27 |
28 | @Override
29 | public ConfigurationMetadata.Hint.ValueProvider getMetadata() {
30 | return this.metadata;
31 | }
32 |
33 |
34 | @NotNull
35 | protected PrefixMatcher getPrefixMatcher(@Nullable PrefixMatcher prefixMatcher) {
36 | return Objects.requireNonNullElse(prefixMatcher, PrefixMatcher.ALWAYS_TRUE);
37 | }
38 |
39 |
40 | @NotNull
41 | protected Project getProject(@NotNull CompletionParameters completionParameters) {
42 | return completionParameters.getPosition().getProject();
43 | }
44 |
45 |
46 | @NotNull
47 | protected Module getModule(@NotNull CompletionParameters completionParameters) {
48 | return Objects.requireNonNull(ModuleUtilCore.findModuleForPsiElement(completionParameters.getPosition()));
49 | }
50 |
51 |
52 | @Contract("_,_,!null->!null")
53 | @Nullable
54 | protected T getParameter(String key, Class type, @Nullable T defaultValue) {
55 | Map parameters = metadata.getParameters();
56 | if (parameters != null) {
57 | Object value = parameters.get(key);
58 | if (value == null) return defaultValue;
59 | if (conversionService.canConvert(value.getClass(), type)) {
60 | return conversionService.convert(value, type);
61 | } else {
62 | throw new IllegalArgumentException("Cannot convert value [" + value + "] to required type: " + type);
63 | }
64 | } else {
65 | return defaultValue;
66 | }
67 | }
68 |
69 |
70 | protected T getRequiredParameter(String key, Class type) {
71 | return Objects.requireNonNull(getParameter(key, type, null),
72 | "Parameter " + key + " is mandatory");
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/index/MetadataIndex.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.index;
2 |
3 | import com.intellij.openapi.project.Project;
4 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.PropertyName;
5 | import org.jetbrains.annotations.NotNull;
6 | import org.jetbrains.annotations.Nullable;
7 |
8 | import java.util.Collections;
9 | import java.util.List;
10 | import java.util.Map;
11 |
12 | public interface MetadataIndex {
13 | static MetadataIndex empty(Project project) {
14 | return new Empty(project);
15 | }
16 |
17 | boolean isEmpty();
18 |
19 | @NotNull Project project();
20 |
21 | /**
22 | * Source file url or source type FQN, maybe empty string.
23 | */
24 | @NotNull List getSource();
25 |
26 | @NotNull Map getGroups();
27 |
28 | @NotNull Map getProperties();
29 |
30 | @NotNull Map getHints();
31 |
32 | @Nullable MetadataGroup getGroup(String name);
33 |
34 | @Nullable MetadataProperty getProperty(String name);
35 |
36 | @Nullable MetadataProperty getNearestParentProperty(String name);
37 |
38 | @Nullable MetadataHint getHint(String name);
39 |
40 | @Nullable MetadataItem getPropertyOrGroup(String name);
41 |
42 | @Nullable NameTreeNode findInNameTrie(String prefix);
43 |
44 | //region empty implement
45 | record Empty(Project project) implements MetadataIndex {
46 | @Override
47 | public boolean isEmpty() {
48 | return true;
49 | }
50 |
51 |
52 | @Override
53 | public @NotNull Project project() {
54 | return project;
55 | }
56 |
57 |
58 | @Override
59 | public @NotNull List getSource() {
60 | return Collections.emptyList();
61 | }
62 |
63 |
64 | @Override
65 | public @Nullable MetadataGroup getGroup(String name) {
66 | return null;
67 | }
68 |
69 |
70 | @Override
71 | public MetadataProperty getProperty(String name) {
72 | return null;
73 | }
74 |
75 |
76 | @Override
77 | public MetadataProperty getNearestParentProperty(String name) {
78 | return null;
79 | }
80 |
81 |
82 | @Override
83 | public MetadataHint getHint(String name) {
84 | return null;
85 | }
86 |
87 |
88 | @Override
89 | public @NotNull Map getGroups() {
90 | return Map.of();
91 | }
92 |
93 |
94 | @Override
95 | public @NotNull Map getProperties() {
96 | return Map.of();
97 | }
98 |
99 |
100 | @Override
101 | public @NotNull Map getHints() {
102 | return Map.of();
103 | }
104 |
105 |
106 | @Override
107 | public MetadataItem getPropertyOrGroup(String name) {
108 | return null;
109 | }
110 |
111 |
112 | @Override
113 | public @Nullable NameTreeNode findInNameTrie(String prefix) {
114 | return null;
115 | }
116 | }
117 | //endregion
118 | }
119 |
--------------------------------------------------------------------------------
/plugin-test/maven/multi-module/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | com.acme
7 | multi-module
8 | 0.0.1-SNAPSHOT
9 | jar
10 |
11 | multi-module
12 | Demo project for Spring Boot
13 |
14 |
15 | org.springframework.boot
16 | spring-boot-starter-parent
17 | 2.0.0.M6
18 |
19 |
20 |
21 |
22 | UTF-8
23 | UTF-8
24 | 1.8
25 | Finchley.M4
26 |
27 |
28 |
29 | child1
30 | child2
31 |
32 |
33 |
34 |
35 | org.springframework.boot
36 | spring-boot-devtools
37 | test
38 |
39 |
40 |
41 |
42 |
43 |
44 | org.springframework.cloud
45 | spring-cloud-dependencies
46 | ${spring-cloud.version}
47 | pom
48 | import
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | org.springframework.boot
57 | spring-boot-maven-plugin
58 |
59 |
60 |
61 |
62 |
63 |
64 | spring-snapshots
65 | Spring Snapshots
66 | https://repo.spring.io/snapshot
67 |
68 | true
69 |
70 |
71 |
72 | spring-milestones
73 | Spring Milestones
74 | https://repo.spring.io/milestone
75 |
76 | false
77 |
78 |
79 |
80 |
81 |
82 |
83 | spring-snapshots
84 | Spring Snapshots
85 | https://repo.spring.io/snapshot
86 |
87 | true
88 |
89 |
90 |
91 | spring-milestones
92 | Spring Milestones
93 | https://repo.spring.io/milestone
94 |
95 | false
96 |
97 |
98 |
99 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/plugin-test/maven/multi-module/child2/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 |
7 | com.acme
8 | multi-module
9 | 0.0.1-SNAPSHOT
10 |
11 |
12 | child2
13 | child2
14 | Child2
15 |
16 |
17 | UTF-8
18 | UTF-8
19 | 1.8
20 | Finchley.M4
21 |
22 |
23 |
24 |
25 | org.springframework.boot
26 | spring-boot
27 |
28 |
29 | org.springframework.boot
30 | spring-boot-configuration-processor
31 | true
32 |
33 |
34 | org.projectlombok
35 | lombok
36 | true
37 |
38 |
39 |
40 |
41 |
42 |
43 | org.springframework.boot
44 | spring-boot-maven-plugin
45 |
46 |
47 |
48 |
49 |
50 |
51 | spring-snapshots
52 | Spring Snapshots
53 | https://repo.spring.io/snapshot
54 |
55 | true
56 |
57 |
58 |
59 | spring-milestones
60 | Spring Milestones
61 | https://repo.spring.io/milestone
62 |
63 | false
64 |
65 |
66 |
67 |
68 |
69 |
70 | spring-snapshots
71 | Spring Snapshots
72 | https://repo.spring.io/snapshot
73 |
74 | true
75 |
76 |
77 |
78 | spring-milestones
79 | Spring Milestones
80 | https://repo.spring.io/milestone
81 |
82 | false
83 |
84 |
85 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/index/hint/provider/ClassReferenceValueProvider.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.hint.provider;
2 |
3 | import com.intellij.codeInsight.completion.CompletionParameters;
4 | import com.intellij.codeInsight.completion.JavaInheritorsGetter;
5 | import com.intellij.codeInsight.completion.PrefixMatcher;
6 | import com.intellij.openapi.project.Project;
7 | import com.intellij.psi.PsiClass;
8 | import com.intellij.psi.PsiClassType;
9 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.hint.Hint;
10 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationMetadata;
11 | import dev.flikas.spring.boot.assistant.idea.plugin.misc.PsiTypeUtils;
12 | import org.apache.commons.lang3.StringUtils;
13 | import org.jetbrains.annotations.NotNull;
14 | import org.jetbrains.annotations.Nullable;
15 |
16 | import java.util.ArrayList;
17 | import java.util.Collection;
18 | import java.util.Collections;
19 | import java.util.List;
20 | import java.util.Optional;
21 |
22 | /**
23 | * @see ConfigurationMetadata.Hint.ValueProvider.Type#CLASS_REFERENCE
24 | */
25 | public class ClassReferenceValueProvider extends AbstractValueProvider {
26 | private final String targetFQN;
27 | /**
28 | * Specify whether only concrete classes are to be considered as valid candidates. Default: true.
29 | */
30 | private final boolean concrete;
31 |
32 |
33 | ClassReferenceValueProvider(ConfigurationMetadata.Hint.ValueProvider metadata) {
34 | super(metadata);
35 |
36 | this.targetFQN = getParameter("target", String.class, null);
37 | this.concrete = getParameter("concrete", Boolean.class, true);
38 | }
39 |
40 |
41 | /**
42 | * The fully qualified name of the class that should be assignable to the chosen value.
43 | * Typically used to filter out non-candidate classes.
44 | * Note that this information can be provided by the type itself by exposing a class with the appropriate upper bound.
45 | */
46 | public Optional getUpperBoundClass(Project project) {
47 | return Optional.ofNullable(targetFQN)
48 | .map(fqn -> PsiTypeUtils.getJavaTypeByName(project, fqn));
49 | }
50 |
51 |
52 | @Override
53 | public Collection provideValues(
54 | @NotNull CompletionParameters completionParameters, @Nullable PrefixMatcher prefixMatcher
55 | ) {
56 | return getUpperBoundClass(completionParameters.getEditor().getProject()).map(baseClass -> {
57 | List values = new ArrayList<>();
58 | // We do not give the specified prefixMatcher to JavaInheritorsGetter, because it will use it with the simple name
59 | // of the class, that will filter out too many candidates we need.
60 | JavaInheritorsGetter.processInheritors(completionParameters, Collections.singleton(baseClass),
61 | PrefixMatcher.ALWAYS_TRUE,
62 | t -> {
63 | PsiClass c = PsiTypeUtils.resolveClassInType(t);
64 | if (c != null) {
65 | if (this.concrete && !PsiTypeUtils.isConcrete(t)) return;
66 | String name = c.getQualifiedName();
67 | if (StringUtils.isBlank(name)) return;
68 | if (prefixMatcher != null && !prefixMatcher.prefixMatches(name)) return;
69 | values.add(new Hint(name, c));
70 | }
71 | });
72 | return values;
73 | }).orElseGet(Collections::emptyList);
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/service/MetadataFileContainer.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.service;
2 |
3 | import com.intellij.openapi.diagnostic.Logger;
4 | import com.intellij.openapi.project.Project;
5 | import com.intellij.openapi.vfs.VirtualFile;
6 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.AggregatedMetadataIndex;
7 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.ConfigurationMetadataIndex;
8 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.FileMetadataSource;
9 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.MetadataIndex;
10 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.MetadataProperty;
11 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationMetadata;
12 | import dev.flikas.spring.boot.assistant.idea.plugin.misc.MutableReference;
13 | import dev.flikas.spring.boot.assistant.idea.plugin.misc.PsiTypeUtils;
14 | import org.jetbrains.annotations.NotNull;
15 | import org.jetbrains.annotations.Nullable;
16 |
17 | import java.io.IOException;
18 | import java.util.Optional;
19 |
20 | /**
21 | * A container of a loaded metadata file, can auto-reload while the file has changed or removed.
22 | */
23 | class MetadataFileContainer implements MutableReference {
24 | private static final Logger LOG = Logger.getInstance(ProjectMetadataService.class);
25 | @NotNull private final FileMetadataSource source;
26 | @NotNull private final Project project;
27 | private MetadataIndex metadata;
28 |
29 |
30 | MetadataFileContainer(@NotNull VirtualFile metadataFile, @NotNull Project project) {
31 | this.source = new FileMetadataSource(metadataFile);
32 | this.project = project;
33 | refresh();
34 | }
35 |
36 |
37 | @Override
38 | public @Nullable MetadataIndex dereference() {
39 | refresh();
40 | return this.metadata;
41 | }
42 |
43 |
44 | @Override
45 | public synchronized void refresh() {
46 | if (!this.source.isValid()) {
47 | if (!this.source.tryReloadIfInvalid()) {
48 | this.metadata = null;
49 | return;
50 | }
51 | } else if (!this.source.isChanged()) {
52 | return;
53 | }
54 | try {
55 | AggregatedMetadataIndex index = new AggregatedMetadataIndex(
56 | new ConfigurationMetadataIndex(this.source, this.project));
57 | // Spring does not create metadata for types in collections, we should create it by ourselves and expand our index,
58 | // to better support code-completion, documentation, navigation, etc.
59 | for (MetadataProperty property : index.getProperties().values()) {
60 | resolvePropertyType(property).ifPresent(index::addFirst);
61 | }
62 | this.metadata = index;
63 | } catch (IOException e) {
64 | LOG.warn("Read metadata file " + this.source.getPresentation() + " failed", e);
65 | }
66 | }
67 |
68 |
69 | /**
70 | * @see ConfigurationMetadata.Property#getType()
71 | */
72 | @NotNull
73 | private Optional resolvePropertyType(@NotNull MetadataProperty property) {
74 | return property.getFullType().filter(t -> PsiTypeUtils.isCollectionOrMap(project, t))
75 | .flatMap(t -> project.getService(ProjectClassMetadataService.class).getMetadata(property.getNameStr(), t));
76 | }
77 |
78 |
79 | @Override
80 | public String toString() {
81 | return "Metadata index form " + this.source.getPresentation();
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/completion/properties/PropertiesCompletionProvider.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.completion.properties;
2 |
3 | import com.intellij.codeInsight.completion.CompletionParameters;
4 | import com.intellij.codeInsight.completion.CompletionProvider;
5 | import com.intellij.codeInsight.completion.CompletionResultSet;
6 | import com.intellij.lang.properties.psi.Property;
7 | import com.intellij.openapi.application.ReadAction;
8 | import com.intellij.openapi.module.Module;
9 | import com.intellij.openapi.project.DumbModeBlockedFunctionality;
10 | import com.intellij.openapi.project.DumbService;
11 | import com.intellij.openapi.project.Project;
12 | import com.intellij.psi.PsiComment;
13 | import com.intellij.psi.PsiElement;
14 | import com.intellij.psi.util.PsiTreeUtil;
15 | import com.intellij.util.ProcessingContext;
16 | import dev.flikas.spring.boot.assistant.idea.plugin.completion.CompletionService;
17 | import org.apache.commons.lang3.StringUtils;
18 | import org.jetbrains.annotations.NotNull;
19 | import org.jetbrains.annotations.Nullable;
20 |
21 | import static com.intellij.codeInsight.completion.CompletionUtil.DUMMY_IDENTIFIER_TRIMMED;
22 | import static com.intellij.openapi.module.ModuleUtilCore.findModuleForPsiElement;
23 |
24 | class PropertiesCompletionProvider extends CompletionProvider {
25 | @Override
26 | protected void addCompletions(
27 | @NotNull CompletionParameters completionParameters,
28 | @NotNull ProcessingContext processingContext,
29 | @NotNull CompletionResultSet resultSet
30 | ) {
31 | PsiElement element = completionParameters.getPosition();
32 | if (element instanceof PsiComment) return;
33 |
34 | Project project = element.getProject();
35 | if (ReadAction.compute(() -> DumbService.isDumb(project))) {
36 | DumbService.getInstance(project).showDumbModeNotificationForFunctionality(
37 | "Spring configuration completion", DumbModeBlockedFunctionality.CodeCompletion);
38 | return;
39 | }
40 | Module module = findModuleForPsiElement(element);
41 | if (module == null) return;
42 |
43 | // Find context YAMLPsiElement, stop if context is not at the same line.
44 | @Nullable Property context = PsiTreeUtil.getParentOfType(element, Property.class, false);
45 | if (context == null) return;
46 |
47 | String originKey = context.getUnescapedKey();
48 | String originValue = context.getUnescapedValue();
49 |
50 | CompletionService service = CompletionService.getInstance(project);
51 | if (originKey != null && originKey.contains(DUMMY_IDENTIFIER_TRIMMED)) {
52 | // User is asking completion for property key
53 | //TODO Map key completion
54 | String queryString = StringUtils.truncate(originKey, originKey.indexOf(DUMMY_IDENTIFIER_TRIMMED));
55 | if (service.findSuggestionForKey(completionParameters, resultSet, "", queryString,
56 | PropertiesKeyInsertHandler.INSTANCE)) {
57 | resultSet.stopHere();
58 | }
59 | } else if (originValue != null && originValue.contains(DUMMY_IDENTIFIER_TRIMMED)) {
60 | // Value completion
61 | String queryString = StringUtils.truncate(originValue, originValue.indexOf(DUMMY_IDENTIFIER_TRIMMED));
62 | if (StringUtils.isBlank(originKey)) return;
63 | if (service.findSuggestionForValue(completionParameters, resultSet, originKey, queryString,
64 | PropertiesValueInsertHandler.INSTANCE)) {
65 | resultSet.stopHere();
66 | }
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/plugin-test/maven/single-module/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | com.example
7 | single-module
8 | 0.0.1-SNAPSHOT
9 | jar
10 |
11 | single-module
12 | Demo project for Spring Boot
13 |
14 |
15 | org.springframework.boot
16 | spring-boot-starter-parent
17 | 2.0.0.M6
18 |
19 |
20 |
21 |
22 | UTF-8
23 | UTF-8
24 | 1.8
25 | Finchley.M4
26 |
27 |
28 |
29 |
30 | org.springframework.cloud
31 | spring-cloud-starter-gateway
32 |
33 |
34 | org.springframework.cloud
35 | spring-cloud-starter-netflix-hystrix
36 |
37 |
38 |
39 | org.springframework.boot
40 | spring-boot-starter-test
41 | test
42 |
43 |
44 |
45 | org.springframework.boot
46 | spring-boot-devtools
47 | test
48 |
49 |
50 |
51 |
52 |
53 |
54 | org.springframework.cloud
55 | spring-cloud-dependencies
56 | ${spring-cloud.version}
57 | pom
58 | import
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | org.springframework.boot
67 | spring-boot-maven-plugin
68 |
69 |
70 |
71 |
72 |
73 |
74 | spring-snapshots
75 | Spring Snapshots
76 | https://repo.spring.io/snapshot
77 |
78 | true
79 |
80 |
81 |
82 | spring-milestones
83 | Spring Milestones
84 | https://repo.spring.io/milestone
85 |
86 | false
87 |
88 |
89 |
90 |
91 |
92 |
93 | spring-snapshots
94 | Spring Snapshots
95 | https://repo.spring.io/snapshot
96 |
97 | true
98 |
99 |
100 |
101 | spring-milestones
102 | Spring Milestones
103 | https://repo.spring.io/milestone
104 |
105 | false
106 |
107 |
108 |
109 |
110 |
111 |
112 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/editing/YamlJoinLinesHandler.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.editing;
2 |
3 | import com.intellij.codeInsight.editorActions.JoinRawLinesHandlerDelegate;
4 | import com.intellij.openapi.editor.Document;
5 | import com.intellij.openapi.fileTypes.FileTypeManager;
6 | import com.intellij.openapi.vfs.VirtualFile;
7 | import com.intellij.psi.PsiDocumentManager;
8 | import com.intellij.psi.PsiElement;
9 | import com.intellij.psi.PsiFile;
10 | import com.intellij.psi.codeStyle.CodeStyleManager;
11 | import com.intellij.psi.util.PsiTreeUtil;
12 | import dev.flikas.spring.boot.assistant.idea.plugin.filetype.SpringBootConfigurationYamlFileType;
13 | import org.jetbrains.annotations.NotNull;
14 | import org.jetbrains.yaml.YAMLTokenTypes;
15 | import org.jetbrains.yaml.psi.YAMLKeyValue;
16 |
17 | import java.util.Collections;
18 |
19 | /**
20 | * {@code
21 | * server:
22 | * port: 8080
23 | * ==>
24 | * spring.port: 8080
25 | * }
26 | */
27 | public class YamlJoinLinesHandler implements JoinRawLinesHandlerDelegate {
28 | @Override
29 | public int tryJoinLines(@NotNull Document document, @NotNull PsiFile file, int start, int end) {
30 | return CANNOT_JOIN;
31 | }
32 |
33 | /**
34 | * {@inheritDoc}
35 | *
36 | * @param start offset right after the last non-space char of first line;
37 | * @param end offset of first non-space char since the next line.
38 | */
39 | @Override
40 | public int tryJoinRawLines(@NotNull Document document, @NotNull PsiFile file, int start, int end) {
41 | // Take effects only in Spring YAML Configuration files.
42 | VirtualFile virtualFile = file.getVirtualFile();
43 | if (virtualFile == null) return CANNOT_JOIN;
44 | FileTypeManager ftm = FileTypeManager.getInstance();
45 | if (!ftm.isFileOfType(virtualFile, SpringBootConfigurationYamlFileType.INSTANCE)) return CANNOT_JOIN;
46 | // Take effects only when first line represents a key
47 | PsiElement elementAtStartLineEnd = file.findElementAt(start - 1);
48 | if (elementAtStartLineEnd == null || !YAMLTokenTypes.COLON.equals(elementAtStartLineEnd.getNode().getElementType()))
49 | return CANNOT_JOIN;
50 | // Take effects only when next line represents a key
51 | PsiElement elementAtEndLineStart = file.findElementAt(end);
52 | if (elementAtEndLineStart == null ||
53 | !YAMLTokenTypes.SCALAR_KEY.equals(elementAtEndLineStart.getNode().getElementType())) {
54 | return CANNOT_JOIN;
55 | }
56 | // Take effects only when the key at next line is child of first line
57 | YAMLKeyValue parentKeyValue = PsiTreeUtil.getParentOfType(elementAtStartLineEnd, YAMLKeyValue.class);
58 | if (parentKeyValue == null) return CANNOT_JOIN;
59 | YAMLKeyValue childKeyValue = PsiTreeUtil.getParentOfType(elementAtEndLineStart, YAMLKeyValue.class);
60 | if (childKeyValue == null) return CANNOT_JOIN;
61 | if (!PsiTreeUtil.isAncestor(parentKeyValue, childKeyValue, false)) {
62 | return CANNOT_JOIN;
63 | }
64 |
65 | // Join parent and child key
66 | start = elementAtStartLineEnd.getTextOffset();
67 | document.replaceString(start, end, ".");
68 |
69 | // Reformat joined key value
70 | PsiDocumentManager pdm = PsiDocumentManager.getInstance(file.getProject());
71 | pdm.commitDocument(document);
72 | YAMLKeyValue joinedKeyValue = PsiTreeUtil.getParentOfType(file.findElementAt(start), YAMLKeyValue.class);
73 | if (joinedKeyValue != null) {
74 | CodeStyleManager csm = CodeStyleManager.getInstance(joinedKeyValue.getManager());
75 | csm.reformatText(file, Collections.singletonList(joinedKeyValue.getTextRange()));
76 | pdm.doPostponedOperationsAndUnblockDocument(document);
77 | }
78 |
79 | return start;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/plugin-test/gradle/single-project/src/main/resources/application.yaml:
--------------------------------------------------------------------------------
1 | # Completion test
2 | spring:
3 | jpa.hibernate:
4 | # Completion: hint-values
5 | ddl-auto: 'create-drop'
6 | data.cassandra.request.throttler:
7 | # Completion: enum
8 | type: CONCURRENCY_LIMITING
9 | datasource:
10 | # Completion: class-reference
11 | driver-class-name: org.h2.Driver
12 | # Completion: List
13 | sql.init.data-locations:
14 | - 'classpath:banner.txt'
15 | - 'classpath:db/change-log/master.yaml'
16 | - 'classpath*:application.yaml'
17 | # Completion: handle-as org.springframework.core.io.Resource
18 | liquibase.change-log: 'classpath:banner.txt'
19 | # Completion: handle-as org.springframework.util.MimeType
20 | freemarker.content-type: application/json
21 | # Completion: handle-as Charset
22 | server:
23 | servlet.encoding.charset: 'UTF-8'
24 | tomcat.accesslog:
25 | encoding: 'IBM-Thai'
26 | # Completion: handle-as Locale
27 | locale: en_US
28 | logging:
29 | group:
30 | app:
31 | - 'dev.flikas'
32 | spring:
33 | - 'org.springframework'
34 | level:
35 | root: info
36 | org.springframework: debug
37 | ---
38 | # In project metadata
39 | oo:
40 | b: c
41 | example:
42 | server:
43 | address:
44 | port: 8080
45 | value: 123
46 | my:
47 | keys1:
48 | - foo
49 | - bar
50 | jobs:
51 | key:
52 | name: 12 # Needs Lombok plugin, key from Map
53 | cron: xxf # Needs Lombok plugin, key from Map
54 | ---
55 | # Inspection:KeyNotDefined(positive)
56 | logging.level:
57 | # hint: enum
58 | root: debug
59 | # hint: logger-name
60 | org.your-company: info
61 | resilience4j.circuitbreaker.instances:
62 | "backendA":
63 | # In Type: Map
64 | allowHealthIndicatorToFail: false
65 | server:
66 | servlet.encoding:
67 | "mapping":
68 | # In Type: Map
69 | "en-US": "UTF-8"
70 | # For Type: List
71 | jetty.accesslog.ignore-paths:
72 | - p1
73 | - p2
74 | - p3
75 | spring:
76 | profiles:
77 | # Pure key
78 | active: dev
79 | datasource.hikari.data-source-properties:
80 | # For Type: java.util.Properties
81 | any-property: should-be-valid
82 | # For Type: java.util.Properties, can map any depth of keys
83 | jpa.properties:
84 | hibernate:
85 | archive:
86 | # Inspection::KeyNotDefined(positive)
87 | scanner: org.hibernate.boot.archive.scan.internal.StandardScanner
88 | ---
89 | #Inspection::InvalidValue(positive)
90 | # Type: Charset
91 | logging.charset.console: utf-8
92 | server:
93 | # Type: List
94 | jetty.accesslog.ignore-paths:
95 | - p1
96 | - p2
97 | - p3
98 | tomcat:
99 | # Type: int
100 | accept-count: 1
101 | # Type: Duration
102 | connection-timeout: 3m
103 | # variable substitute
104 | threads.max: ${spring.cloud-version}
105 | access-log:
106 | # Type: boolean
107 | buffered: false
108 | spring:
109 | # Type org.springframework.core.io.Resource
110 | banner.location: 'classpath:banner.txt'
111 | profiles:
112 | # Type: List -- String
113 | active: dev
114 | # Type: java.util.Properties
115 | datasource.hikari.data-source-properties:
116 | any-property: should-be-valid
117 | # Type: Map -- any depth of keys
118 | jpa.properties:
119 | hibernate:
120 | archive:
121 | scanner: org.hibernate.boot.archive.scan.internal.StandardScanner
122 | ---
123 | # Inspection::Deprecated(positive)
124 | # Deprecated property but available sub-property
125 | spring:
126 | profiles:
127 | active: dev
128 |
--------------------------------------------------------------------------------
/plugin-test/maven/spring-boot-example/demo-app/src/main/resources/application.yaml:
--------------------------------------------------------------------------------
1 | # Positive test cases, here should no warnings ----------------->
2 |
3 | # Completion test
4 | spring:
5 | # Completion: type boolean
6 | devtools.add-properties: true
7 | jpa.hibernate:
8 | # Completion: hint-values
9 | ddl-auto: 'create-drop'
10 | # Completion: type enum
11 | data.cassandra.request.throttler.type: CONCURRENCY_LIMITING
12 | datasource:
13 | # Completion: class-reference
14 | driver-class-name: org.h2.Driver
15 | # Completion: List
16 | sql.init.data-locations:
17 | - 'classpath:banner.txt'
18 | - 'classpath:db/change-log/master.yaml'
19 | # Completion: handle-as org.springframework.core.io.Resource
20 | liquibase.change-log: 'classpath:banner.txt'
21 | # Completion: handle-as org.springframework.util.MimeType
22 | freemarker.content-type: application/json
23 | # Completion: handle-as Charset
24 | server:
25 | servlet.encoding.charset: 'UTF-8'
26 | tomcat.accesslog:
27 | encoding: 'IBM-Thai'
28 | # Completion: handle-as Locale
29 | locale: en_US
30 | logging:
31 | group:
32 | app:
33 | - 'dev.flikas'
34 | spring:
35 | - 'org.springframework'
36 | level:
37 | root: info
38 | org.springframework: debug
39 | ---
40 | # In project metadata
41 | oo:
42 | b: c
43 | example:
44 | server:
45 | address:
46 | port: 8080
47 | value: 123
48 | my:
49 | keys1:
50 | - foo
51 | - bar
52 | jobs:
53 | key:
54 | name: 12 # Needs Lombok plugin, key from Map
55 | cron: xxf # Needs Lombok plugin, key from Map
56 | ---
57 | # Inspection:KeyNotDefined(positive)
58 | logging.level:
59 | # hint: enum
60 | root: debug
61 | # hint: logger-name
62 | org.your-company: info
63 | resilience4j.circuitbreaker.instances:
64 | "backendA":
65 | # In Type: Map
66 | allowHealthIndicatorToFail: false
67 | server:
68 | servlet.encoding:
69 | "mapping":
70 | # In Type: Map
71 | "en-US": "UTF-8"
72 | # For Type: List
73 | jetty.accesslog.ignore-paths:
74 | - p1
75 | - p2
76 | - p3
77 | spring:
78 | profiles:
79 | # Pure key
80 | active: dev
81 | datasource.hikari.data-source-properties:
82 | # For Type: java.util.Properties
83 | any-property: should-be-valid
84 | # For Type: java.util.Properties, can map any depth of keys
85 | jpa.properties:
86 | hibernate:
87 | archive:
88 | # Inspection::KeyNotDefined(positive)
89 | scanner: org.hibernate.boot.archive.scan.internal.StandardScanner
90 | ---
91 | #Inspection::InvalidValue(positive)
92 | # Type: Charset
93 | logging.charset.console: utf-8
94 | server:
95 | # Type: List
96 | jetty.accesslog.ignore-paths:
97 | - p1
98 | - p2
99 | - p3
100 | tomcat:
101 | # Type: int
102 | accept-count: 1
103 | # Type: Duration
104 | connection-timeout: 3m
105 | # variable substitute
106 | threads.max: ${spring.cloud-version}
107 | access-log:
108 | # Type: boolean
109 | buffered: false
110 | spring:
111 | # Type org.springframework.core.io.Resource
112 | banner.location: 'classpath:banner.txt'
113 | profiles:
114 | # Type: List -- String
115 | active: dev
116 | # Type: java.util.Properties
117 | datasource.hikari.data-source-properties:
118 | any-property: should-be-valid
119 | # Type: Map -- any depth of keys
120 | jpa.properties:
121 | hibernate:
122 | archive:
123 | scanner: org.hibernate.boot.archive.scan.internal.StandardScanner
124 | ---
125 | # Inspection::Deprecated(positive)
126 | # Deprecated property but available sub-property
127 | spring:
128 | profiles:
129 | active: dev
130 |
--------------------------------------------------------------------------------
/plugin-test/gradle/single-project/src/main/java/com/acme/model/DynamicRoot.java:
--------------------------------------------------------------------------------
1 | package com.acme.model;
2 |
3 | import lombok.Getter;
4 | import lombok.Setter;
5 |
6 | import java.math.BigDecimal;
7 | import java.util.Collection;
8 | import java.util.Map;
9 | import java.util.Set;
10 |
11 | /**
12 | * Dynamic root documentation, Intentionally written so long that this should not fit into the suggestions. After previous dot, this section should not be visible in the documentation section
13 | */
14 | public class DynamicRoot {
15 |
16 | public int invisiblePublicProperty;
17 | protected int invisibleProtectedProperty;
18 | int invisiblePackageScopedProperty;
19 | @Getter
20 | int invisibleGetterOnlyProperty;
21 | private int invisiblePrivateProperty;
22 |
23 | /**
24 | * Boolean documentation
25 | */
26 | @Getter
27 | @Setter
28 | private boolean boolProp;
29 | /**
30 | * Byte documentation
31 | */
32 | @Getter
33 | @Setter
34 | private byte byteProp;
35 | /**
36 | * Short documentation
37 | */
38 | @Getter
39 | @Setter
40 | private short shortProp;
41 | /**
42 | * Int documentation
43 | */
44 | @Getter
45 | @Setter
46 | private int intProp;
47 | /**
48 | * Long documentation
49 | */
50 | @Getter
51 | @Setter
52 | private long longProp;
53 | /**
54 | * Float documentation
55 | */
56 | @Getter
57 | @Setter
58 | private float floatProp;
59 | /**
60 | * Double documentation
61 | */
62 | @Getter
63 | @Setter
64 | private double doubleProp;
65 | /**
66 | * Big decimal documentation
67 | */
68 | @Getter
69 | @Setter
70 | private BigDecimal bigDecimalProp;
71 | /**
72 | * Char documentation
73 | */
74 | @Getter
75 | @Setter
76 | private char charProp;
77 | /**
78 | * String documentation
79 | */
80 | @Getter
81 | @Setter
82 | private String stringProp;
83 | /**
84 | * Enum documentation
85 | */
86 | @Getter
87 | @Setter
88 | private DynamicEnum enumProp;
89 | /**
90 | * Primitive key -> value map documentation
91 | */
92 | @Getter
93 | @Setter
94 | private Map primitiveKeyToPrimitiveValueMap;
95 | /**
96 | * Enum key -> primitive value documentation
97 | */
98 | @Getter
99 | @Setter
100 | private Map enumKeyToPrimitiveValueMap;
101 | /**
102 | * Primitive collection documentation
103 | */
104 | @Getter
105 | @Setter
106 | private Collection primitiveCollection;
107 | /**
108 | * Enum collection documentation
109 | */
110 | @Getter
111 | @Setter
112 | private Collection enumCollection;
113 | /**
114 | * Dynamic child documentation
115 | */
116 | @Getter
117 | @Setter
118 | private DynamicChild dynamicChild;
119 | /**
120 | * Primitive key -> dynamic child documentation
121 | */
122 | @Getter
123 | @Setter
124 | private Map primitiveKeyToDynamicChildValueMap;
125 | /**
126 | * enum key -> dynamic child documentation
127 | */
128 | @Getter
129 | @Setter
130 | private Map enumKeyToDynamicChildValueMap;
131 | /**
132 | * dynamic child collection documentation
133 | */
134 | @Getter
135 | @Setter
136 | private Collection childCollection;
137 |
138 | private void setAnotherInvisiblePrivateProperty(int anotherInvisiblePrivateProperty) {
139 | // does not matter
140 | }
141 |
142 | protected void setAnotherInvisibleProtectedProperty(int anotherInvisibleProtectedProperty) {
143 | // does not matter
144 | }
145 |
146 | void setAnotherInvisiblePackageScopedProperty(int anotherInvisiblePackageScopedProperty) {
147 | // does not matter
148 | }
149 |
150 | public void setPropertyViaSetter(int propertyViaSetter) {
151 | // does not matter
152 | }
153 |
154 | }
155 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/editing/YamlSplitKeyProcessor.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.editing;
2 |
3 | import com.intellij.application.options.CodeStyle;
4 | import com.intellij.codeInsight.editorActions.enter.EnterHandlerDelegateAdapter;
5 | import com.intellij.openapi.actionSystem.DataContext;
6 | import com.intellij.openapi.editor.Document;
7 | import com.intellij.openapi.editor.Editor;
8 | import com.intellij.openapi.editor.actionSystem.EditorActionHandler;
9 | import com.intellij.openapi.fileTypes.FileTypeManager;
10 | import com.intellij.openapi.util.Ref;
11 | import com.intellij.openapi.util.TextRange;
12 | import com.intellij.openapi.vfs.VirtualFile;
13 | import com.intellij.psi.PsiDocumentManager;
14 | import com.intellij.psi.PsiElement;
15 | import com.intellij.psi.PsiFile;
16 | import com.intellij.psi.impl.source.codeStyle.IndentHelperImpl;
17 | import com.intellij.psi.util.PsiTreeUtil;
18 | import dev.flikas.spring.boot.assistant.idea.plugin.filetype.SpringBootConfigurationYamlFileType;
19 | import org.jetbrains.annotations.NotNull;
20 | import org.jetbrains.yaml.YAMLTextUtil;
21 | import org.jetbrains.yaml.YAMLTokenTypes;
22 | import org.jetbrains.yaml.psi.YAMLCompoundValue;
23 | import org.jetbrains.yaml.psi.YAMLKeyValue;
24 | import org.jetbrains.yaml.psi.YAMLValue;
25 |
26 | import static com.intellij.codeInsight.editorActions.enter.EnterHandlerDelegate.Result.Continue;
27 |
28 | public class YamlSplitKeyProcessor extends EnterHandlerDelegateAdapter {
29 |
30 | @Override
31 | public Result preprocessEnter(@NotNull PsiFile file, @NotNull Editor editor, @NotNull Ref caretOffset,
32 | @NotNull Ref caretAdvance, @NotNull DataContext dataContext,
33 | EditorActionHandler originalHandler) {
34 | VirtualFile virtualFile = file.getVirtualFile();
35 | if (virtualFile == null) return Continue;
36 | FileTypeManager ftm = FileTypeManager.getInstance();
37 | if (!ftm.isFileOfType(virtualFile, SpringBootConfigurationYamlFileType.INSTANCE))
38 | return Continue;
39 | if (caretOffset.get() <= 0)
40 | return Continue;
41 | PsiDocumentManager.getInstance(file.getProject()).commitDocument(editor.getDocument());
42 | PsiElement elementAtCaret = file.findElementAt(caretOffset.get());
43 | if (elementAtCaret == null)
44 | return Continue;
45 |
46 | if (YAMLTokenTypes.SCALAR_KEY.equals(elementAtCaret.getNode().getElementType())) {
47 | Document document = editor.getDocument();
48 | int offset = caretOffset.get();
49 | char c = document.getText().charAt(offset);
50 | if (c != '.') {
51 | //If caret is right after the dot, it should work as well.
52 | c = document.getText().charAt(--offset);
53 | }
54 | if (c == '.') {
55 | int indentSize = CodeStyle.getIndentSize(file);
56 | //Indent children
57 | YAMLKeyValue keyValue = PsiTreeUtil.getParentOfType(elementAtCaret, YAMLKeyValue.class);
58 | if (keyValue != null) {
59 | YAMLValue valueElement = keyValue.getValue();
60 | if (valueElement instanceof YAMLCompoundValue) {
61 | TextRange range = valueElement.getTextRange();
62 | document.replaceString(range.getStartOffset(), range.getEndOffset(),
63 | YAMLTextUtil.indentText(valueElement.getText(), indentSize));
64 | }
65 | }
66 | //Split the key
67 | String space = IndentHelperImpl.fillIndent(CodeStyle.getIndentOptions(file), indentSize);
68 | document.replaceString(offset, offset + 1, ":" + space);
69 | caretOffset.set(offset + 1);
70 | }
71 | }
72 |
73 | return Continue;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/completion/yaml/YamlCompletionProvider.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.completion.yaml;
2 |
3 | import com.intellij.codeInsight.completion.CompletionParameters;
4 | import com.intellij.codeInsight.completion.CompletionProvider;
5 | import com.intellij.codeInsight.completion.CompletionResultSet;
6 | import com.intellij.openapi.application.ReadAction;
7 | import com.intellij.openapi.module.Module;
8 | import com.intellij.openapi.project.DumbModeBlockedFunctionality;
9 | import com.intellij.openapi.project.DumbService;
10 | import com.intellij.openapi.project.Project;
11 | import com.intellij.psi.PsiComment;
12 | import com.intellij.psi.PsiElement;
13 | import com.intellij.psi.util.PsiTreeUtil;
14 | import com.intellij.util.ProcessingContext;
15 | import dev.flikas.spring.boot.assistant.idea.plugin.completion.CompletionService;
16 | import org.apache.commons.lang3.StringUtils;
17 | import org.jetbrains.annotations.NotNull;
18 | import org.jetbrains.annotations.Nullable;
19 | import org.jetbrains.yaml.YAMLUtil;
20 | import org.jetbrains.yaml.psi.YAMLKeyValue;
21 | import org.jetbrains.yaml.psi.YAMLPsiElement;
22 | import org.jetbrains.yaml.psi.YAMLScalar;
23 | import org.jetbrains.yaml.psi.YAMLSequenceItem;
24 |
25 | import static com.intellij.codeInsight.completion.CompletionUtil.DUMMY_IDENTIFIER_TRIMMED;
26 | import static com.intellij.openapi.module.ModuleUtilCore.findModuleForPsiElement;
27 |
28 | class YamlCompletionProvider extends CompletionProvider {
29 | @Override
30 | protected void addCompletions(
31 | @NotNull final CompletionParameters completionParameters,
32 | @NotNull final ProcessingContext processingContext,
33 | @NotNull final CompletionResultSet resultSet
34 | ) {
35 | PsiElement element = completionParameters.getPosition();
36 | if (element instanceof PsiComment) {
37 | return;
38 | }
39 | Project project = element.getProject();
40 | if (ReadAction.compute(() -> DumbService.isDumb(project))) {
41 | DumbService.getInstance(project).showDumbModeNotificationForFunctionality("Spring configuration completion",
42 | DumbModeBlockedFunctionality.CodeCompletion);
43 | return;
44 | }
45 | Module module = findModuleForPsiElement(element);
46 | if (module == null) {
47 | return;
48 | }
49 |
50 | // Find context YAMLPsiElement, stop if context is not at the same line.
51 | @Nullable YAMLPsiElement context = PsiTreeUtil.getParentOfType(element, YAMLPsiElement.class, false);
52 | if (context == null) return;
53 | if (!YAMLUtil.psiAreAtTheSameLine(element, context)) return;
54 |
55 | String queryString = element.getText();
56 | String ancestorKeys = YAMLUtil.getConfigFullName(context);
57 |
58 | ancestorKeys = StringUtils.removeEnd(ancestorKeys, queryString);
59 | // use chars before the completion point as query string, ignore the remains,
60 | // besides, if user press Tab for completion, we should delete thr remains chars.
61 | queryString = StringUtils.truncate(queryString, queryString.indexOf(DUMMY_IDENTIFIER_TRIMMED));
62 | CompletionService service = CompletionService.getInstance(project);
63 | YAMLKeyValue nearestKeyValue = PsiTreeUtil.getParentOfType(context, YAMLKeyValue.class, false);
64 | YAMLSequenceItem nearestSequenceItem = PsiTreeUtil.getParentOfType(context, YAMLSequenceItem.class, false);
65 | if (((nearestKeyValue != null && YAMLUtil.psiAreAtTheSameLine(nearestKeyValue, context))
66 | || (nearestSequenceItem != null && YAMLUtil.psiAreAtTheSameLine(nearestSequenceItem, context)))
67 | && context instanceof YAMLScalar) {
68 | // User is asking completion for property value
69 | service.findSuggestionForValue(completionParameters, resultSet, ancestorKeys, queryString,
70 | YamlValueInsertHandler.INSTANCE);
71 | } else {
72 | // Key completion
73 | service.findSuggestionForKey(completionParameters, resultSet, ancestorKeys, queryString,
74 | YamlKeyInsertHandler.INSTANCE);
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/index/NameTreeNode.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.index;
2 |
3 | import com.intellij.openapi.diagnostic.Logger;
4 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.PropertyName;
5 | import lombok.AccessLevel;
6 | import lombok.Data;
7 | import lombok.Getter;
8 | import lombok.Setter;
9 | import org.apache.commons.collections4.Trie;
10 | import org.apache.commons.collections4.trie.PatriciaTrie;
11 | import org.apache.commons.collections4.trie.UnmodifiableTrie;
12 | import org.jetbrains.annotations.Nullable;
13 |
14 | import java.util.Collections;
15 | import java.util.LinkedList;
16 | import java.util.List;
17 |
18 | import static dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationPropertyName.Form.UNIFORM;
19 |
20 | @Data
21 | public class NameTreeNode {
22 | private static final Logger LOG = Logger.getInstance(NameTreeNode.class);
23 | private final PatriciaTrie children = new PatriciaTrie<>();
24 | private final List data = new LinkedList<>();
25 | @Getter(AccessLevel.NONE)
26 | @Setter(AccessLevel.NONE)
27 | private IndexedType indexedType = IndexedType.NONE;
28 |
29 |
30 | public static NameTreeNode merge(NameTreeNode n1, NameTreeNode n2) {
31 | NameTreeNode dst = new NameTreeNode();
32 | if (!n1.children.isEmpty() && !n2.children.isEmpty()) {
33 | assert n1.indexedType == n2.indexedType;
34 | dst.indexedType = n1.indexedType;
35 | } else if (!n1.children.isEmpty()) {
36 | dst.indexedType = n1.indexedType;
37 | } else if (!n2.children.isEmpty()) {
38 | dst.indexedType = n2.indexedType;
39 | }
40 | dst.data.addAll(n1.data);
41 | dst.data.addAll(n2.data);
42 | n1.children.forEach((k, v) -> dst.children.merge(k, v, NameTreeNode::merge));
43 | n2.children.forEach((k, v) -> dst.children.merge(k, v, NameTreeNode::merge));
44 | return dst;
45 | }
46 |
47 |
48 | public Trie getChildren() {
49 | return UnmodifiableTrie.unmodifiableTrie(children);
50 | }
51 |
52 |
53 | public boolean isIndexed() {
54 | return this.indexedType != IndexedType.NONE;
55 | }
56 |
57 |
58 | @Nullable
59 | public NameTreeNode findChild(PropertyName name) {
60 | if (name.isEmpty()) return this;
61 | NameTreeNode child;
62 | if (this.indexedType == IndexedType.NON_NUMERIC) {
63 | assert this.children.size() == 1;
64 | child = this.children.values().iterator().next();
65 | } else if (this.indexedType == IndexedType.NUMERIC) {
66 | assert this.children.size() == 1;
67 | if (name.isNumericIndex(0)) {
68 | child = this.children.values().iterator().next();
69 | } else {
70 | return null;
71 | }
72 | } else {
73 | child = this.children.get(name.getElement(0, UNIFORM));
74 | }
75 | if (child == null) {
76 | return null;
77 | } else {
78 | return child.findChild(name.subName(1));
79 | }
80 | }
81 |
82 |
83 | public void addChild(PropertyName name, MetadataItem value) {
84 | if (name.isEmpty()) {
85 | this.data.add(value);
86 | return;
87 | }
88 | String key;
89 | if (name.isAnyNonNumericIndex(0)) {
90 | key = "*";
91 | this.indexedType = IndexedType.NON_NUMERIC;
92 | ensureAtMostOneChild(key);
93 | } else if (name.isAnyNumericIndex(0)) {
94 | key = "#";
95 | this.indexedType = IndexedType.NUMERIC;
96 | ensureAtMostOneChild(key);
97 | } else {
98 | key = name.getElement(0, UNIFORM);
99 | }
100 | NameTreeNode child = this.children.computeIfAbsent(key, k -> new NameTreeNode());
101 | child.addChild(name.subName(1), value);
102 | }
103 |
104 |
105 | private void ensureAtMostOneChild(String key) {
106 | if (!this.children.isEmpty() && !this.children.keySet().equals(Collections.singleton(key))) {
107 | LOG.warn("There should be at most one child of key \"" + key + "\", but children are: " + this.children);
108 | this.children.clear();
109 | }
110 | }
111 |
112 |
113 | enum IndexedType {NUMERIC, NON_NUMERIC, NONE}
114 | }
115 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/misc/CompositeIconUtils.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.misc;
2 |
3 | import com.intellij.ui.IconManager;
4 | import com.intellij.ui.JBColor;
5 | import com.intellij.ui.OffsetIcon;
6 | import com.intellij.ui.RetrievableIcon;
7 | import com.intellij.ui.icons.IconReplacer;
8 | import com.intellij.util.IconUtil;
9 | import com.intellij.util.ui.ColorIcon;
10 | import com.intellij.util.ui.JBCachingScalableIcon;
11 | import org.jetbrains.annotations.NotNull;
12 |
13 | import javax.swing.*;
14 | import java.awt.*;
15 | import java.util.Objects;
16 |
17 | import static com.intellij.ui.scale.ScaleType.OBJ_SCALE;
18 | import static java.lang.Math.ceil;
19 | import static java.lang.Math.floor;
20 |
21 | public interface CompositeIconUtils {
22 | static Icon createWithModifier(Icon baseIcon, Icon modifierIcon) {
23 | IconManager im = IconManager.getInstance();
24 | int halfWidth = OffsetIcon.REGULAR_OFFSET / 2;
25 | Icon modifierBackground = new ColorIcon(modifierIcon.getIconWidth(), modifierIcon.getIconHeight(),
26 | modifierIcon.getIconWidth(), modifierIcon.getIconHeight(),
27 | JBColor.background(), false);
28 | Icon modifierWithBackground = im.createLayered(modifierBackground, modifierIcon);
29 | var maskLayer = new ModifierIcon(IconUtil.resizeSquared(modifierWithBackground, halfWidth));
30 | return im.createLayered(baseIcon, maskLayer);
31 | }
32 |
33 |
34 | class ModifierIcon extends JBCachingScalableIcon implements RetrievableIcon {
35 | private final double factor = 0.8;
36 | private final Icon myIcon;
37 | private int myWidth;
38 | private int myHeight;
39 | private Icon myScaledIcon;
40 | private int myScaledXOffset;
41 | private int myScaledYOffset;
42 |
43 | {
44 | getScaleContext().addUpdateListener(this::updateSize);
45 | setAutoUpdateScaleContext(false);
46 | }
47 |
48 |
49 | public ModifierIcon(@NotNull Icon icon) {
50 | myIcon = icon;
51 | updateSize();
52 | }
53 |
54 | private ModifierIcon(@NotNull ModifierIcon icon) {
55 | super(icon);
56 | myWidth = icon.myWidth;
57 | myHeight = icon.myHeight;
58 | myIcon = icon.myIcon;
59 | myScaledIcon = null;
60 | myScaledXOffset = icon.myScaledXOffset;
61 | myScaledYOffset = icon.myScaledYOffset;
62 | }
63 |
64 | @Override
65 | public @NotNull ModifierIcon copy() {
66 | return new ModifierIcon(this);
67 | }
68 |
69 | public @NotNull Icon getIcon() {
70 | return myIcon;
71 | }
72 |
73 | public int hashCode() {
74 | return myIcon.hashCode();
75 | }
76 |
77 | public boolean equals(Object obj) {
78 | if (obj == this) return true;
79 | if (obj instanceof ModifierIcon icon) {
80 | return Objects.equals(icon.myIcon, myIcon);
81 | }
82 | return false;
83 | }
84 |
85 | @Override
86 | public void paintIcon(Component c, Graphics g, int x, int y) {
87 | getScaleContext().update();
88 | if (myScaledIcon == null) {
89 | float scale = getScale();
90 | myScaledIcon = scale == 1f ? myIcon : IconUtil.scale(myIcon, null, scale);
91 | }
92 | myScaledIcon.paintIcon(c, g, myScaledXOffset + x, myScaledYOffset + y);
93 | }
94 |
95 | @Override
96 | public int getIconWidth() {
97 | getScaleContext().update();
98 | return (int) ceil(scaleVal(myWidth, OBJ_SCALE) * (1 + factor));
99 | }
100 |
101 | @Override
102 | public int getIconHeight() {
103 | getScaleContext().update();
104 | return (int) ceil(scaleVal(myHeight, OBJ_SCALE) * (1 + factor));
105 | }
106 |
107 | @Override
108 | public @NotNull Icon replaceBy(@NotNull IconReplacer replacer) {
109 | return new ModifierIcon(replacer.replaceIcon(myIcon));
110 | }
111 |
112 | private void updateSize() {
113 | myWidth = myIcon.getIconWidth();
114 | myHeight = myIcon.getIconHeight();
115 | myScaledXOffset = (int) floor(scaleVal(myWidth, OBJ_SCALE) * factor);
116 | myScaledYOffset = (int) floor(scaleVal(myHeight, OBJ_SCALE) * factor);
117 | }
118 |
119 | @Override
120 | public String toString() {
121 | return "ModifierIcon: icon=" + myIcon;
122 | }
123 |
124 | @Override
125 | public @NotNull Icon retrieveIcon() {
126 | return getIcon();
127 | }
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/index/MetadataGroupImpl.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.index;
2 |
3 | import com.intellij.lang.documentation.DocumentationMarkup;
4 | import com.intellij.openapi.util.text.HtmlBuilder;
5 | import com.intellij.psi.PsiClass;
6 | import com.intellij.psi.PsiMethod;
7 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationMetadata;
8 | import dev.flikas.spring.boot.assistant.idea.plugin.misc.PsiElementUtils;
9 | import dev.flikas.spring.boot.assistant.idea.plugin.misc.PsiMethodUtils;
10 | import dev.flikas.spring.boot.assistant.idea.plugin.misc.PsiTypeUtils;
11 | import lombok.EqualsAndHashCode;
12 | import lombok.Getter;
13 | import lombok.ToString;
14 | import org.apache.commons.lang3.StringUtils;
15 | import org.jetbrains.annotations.NotNull;
16 |
17 | import java.util.Optional;
18 |
19 | @EqualsAndHashCode(of = "metadata")
20 | @ToString(of = "metadata")
21 | class MetadataGroupImpl implements MetadataGroup {
22 | private final MetadataIndex index;
23 | @Getter
24 | private final ConfigurationMetadata.Group metadata;
25 | private volatile String renderedDocument = null;
26 |
27 |
28 | MetadataGroupImpl(MetadataIndex index, ConfigurationMetadata.Group metadata) {
29 | this.index = index;
30 | this.metadata = metadata;
31 | }
32 |
33 |
34 | @Override
35 | public @NotNull String getNameStr() {
36 | return metadata.getName();
37 | }
38 |
39 |
40 | /**
41 | * @see ConfigurationMetadata.Group#getType()
42 | */
43 | @Override
44 | public Optional getType() {
45 | return Optional.ofNullable(metadata.getType())
46 | .filter(StringUtils::isNotBlank)
47 | .map(type -> PsiTypeUtils.findClass(index.project(), type));
48 | }
49 |
50 |
51 | /**
52 | * @see ConfigurationMetadata.Group#getSourceType()
53 | */
54 | @Override
55 | public Optional getSourceType() {
56 | return Optional.ofNullable(metadata.getSourceType())
57 | .filter(StringUtils::isNotBlank)
58 | .map(type -> PsiTypeUtils.findClass(index.project(), type));
59 | }
60 |
61 |
62 | @Override
63 | public @NotNull String getRenderedDescription() {
64 | if (this.renderedDocument != null) {
65 | return this.renderedDocument;
66 | }
67 | synchronized (this) {
68 | if (this.renderedDocument != null) {
69 | return this.renderedDocument;
70 | }
71 | HtmlBuilder doc = new HtmlBuilder();
72 | String desc = metadata.getDescription();
73 | //Unfortunately, even though there is a 'description' field for the group metadata, `spring boot configuration processor` will never fill it.
74 | //Here we use group class/method's document instead.
75 | String descFrom = null;
76 | if (StringUtils.isBlank(desc)) {
77 | desc = getSourceMethod().map(PsiElementUtils::getDocument).orElse(null);
78 | descFrom = getSourceMethod().map(PsiElementUtils::createLinkForDoc).orElse(null);
79 | }
80 | if (StringUtils.isBlank(desc)) {
81 | desc = getType().map(PsiElementUtils::getDocument).orElse(null);
82 | descFrom = getType().map(PsiElementUtils::createLinkForDoc).orElse(null);
83 | }
84 | if (StringUtils.isBlank(desc)) {
85 | desc = getSourceType().map(PsiElementUtils::getDocument).orElse(null);
86 | descFrom = getSourceType().map(PsiElementUtils::createLinkForDoc).orElse(null);
87 | }
88 | if (StringUtils.isNotBlank(desc)) {
89 | if (StringUtils.isNotBlank(descFrom)) {
90 | doc.append(DocumentationMarkup.GRAYED_ELEMENT
91 | .addText("(Doc below is copied from ")
92 | .addRaw(descFrom)
93 | .addText(")\n"));
94 | }
95 | doc.appendRaw(desc);
96 | }
97 | this.renderedDocument = doc.toString();
98 | }
99 | return this.renderedDocument;
100 | }
101 |
102 |
103 | @Override
104 | public MetadataIndex getIndex() {
105 | return index;
106 | }
107 |
108 |
109 | /**
110 | * @see ConfigurationMetadata.Group#getSourceMethod()
111 | */
112 | @Override
113 | public Optional getSourceMethod() {
114 | String sourceMethod = metadata.getSourceMethod();
115 | if (StringUtils.isBlank(sourceMethod)) return Optional.empty();
116 | return getSourceType().flatMap(sourceClass -> PsiMethodUtils.findMethodBySignature(sourceClass, sourceMethod));
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/misc/PsiElementUtils.java:
--------------------------------------------------------------------------------
1 | package dev.flikas.spring.boot.assistant.idea.plugin.misc;
2 |
3 | import com.intellij.codeInsight.documentation.DocumentationManagerUtil;
4 | import com.intellij.codeInsight.javadoc.JavaDocInfoGenerator;
5 | import com.intellij.lang.documentation.DocumentationMarkup;
6 | import com.intellij.openapi.diagnostic.Logger;
7 | import com.intellij.openapi.fileTypes.FileType;
8 | import com.intellij.openapi.fileTypes.FileTypeManager;
9 | import com.intellij.openapi.vfs.VirtualFile;
10 | import com.intellij.psi.PsiClass;
11 | import com.intellij.psi.PsiDocCommentOwner;
12 | import com.intellij.psi.PsiElement;
13 | import com.intellij.psi.PsiJavaDocumentedElement;
14 | import com.intellij.psi.PsiJvmMember;
15 | import com.intellij.psi.PsiMethod;
16 | import com.intellij.psi.PsiModifierList;
17 | import com.intellij.psi.javadoc.PsiDocComment;
18 | import com.intellij.psi.util.PsiUtil;
19 | import lombok.experimental.UtilityClass;
20 | import org.jetbrains.annotations.NotNull;
21 | import org.jetbrains.annotations.Nullable;
22 |
23 | @UtilityClass
24 | public class PsiElementUtils {
25 | private static final Logger LOG = Logger.getInstance(PsiElementUtils.class);
26 |
27 |
28 | public static boolean isInFileOfType(PsiElement element, FileType fileType) {
29 | VirtualFile virtualFile = PsiUtil.getVirtualFile(element);
30 | if (virtualFile == null) {
31 | return false;
32 | }
33 | FileTypeManager ftm = FileTypeManager.getInstance();
34 | return ftm.isFileOfType(virtualFile, fileType);
35 | }
36 |
37 |
38 | public static String getDocument(PsiJavaDocumentedElement element) {
39 | JavaDocInfoGenerator generator = new JavaDocInfoGenerator(element.getProject(), element);
40 | PsiDocComment comment = getDocComment(element);
41 | if (comment == null) return null;
42 | StringBuilder doc = new StringBuilder();
43 | generator.generateCommonSection(doc, comment);
44 | // We use the CONTENT part of document generated by JavaDocInfoGenerator.generateCommonSection only,
45 | // because the SECTIONS part is not complete, and we don't want it influence user.
46 | int idx = doc.indexOf(DocumentationMarkup.CONTENT_END);
47 | if (idx >= 0) {
48 | doc.delete(idx, doc.length());
49 | idx = doc.indexOf(DocumentationMarkup.CONTENT_START);
50 | assert idx >= 0;
51 | doc.delete(idx, idx + DocumentationMarkup.CONTENT_START.length());
52 | } else {
53 | return null;
54 | }
55 | return doc.toString().strip();
56 | }
57 |
58 |
59 | public static String createLinkForDoc(@NotNull PsiJvmMember member) {
60 | String label;
61 | String ref;
62 | PsiClass containingClass = member.getContainingClass();
63 | if (containingClass == null) {
64 | if (member instanceof PsiClass psiClass) {
65 | label = psiClass.getQualifiedName();
66 | ref = psiClass.getQualifiedName();
67 | } else {
68 | label = member.getName();
69 | ref = member.getName();
70 | }
71 | } else {
72 | label = containingClass.getQualifiedName() + "."
73 | + member.getName() + (member instanceof PsiMethod ? "()" : "");
74 | ref = containingClass.getQualifiedName() + "#" + member.getName();
75 | }
76 | return createHyperLink(ref, label);
77 | }
78 |
79 |
80 | private static @NotNull String createHyperLink(String ref, String label) {
81 | StringBuilder buffer = new StringBuilder();
82 | DocumentationManagerUtil.createHyperlink(buffer, ref, label, false);
83 | return buffer.toString();
84 | }
85 |
86 |
87 | private static @Nullable PsiDocComment getDocComment(PsiJavaDocumentedElement docOwner) {
88 | PsiElement navElement = docOwner.getNavigationElement();
89 | if (!(navElement instanceof PsiJavaDocumentedElement)) {
90 | LOG.info("Wrong navElement: " + navElement + "; original = " + docOwner + " of class " + docOwner.getClass());
91 | return null;
92 | }
93 | PsiDocComment comment = ((PsiJavaDocumentedElement) navElement).getDocComment();
94 | if (comment == null) { //check for non-normalized fields
95 | PsiModifierList modifierList = docOwner instanceof PsiDocCommentOwner
96 | ? ((PsiDocCommentOwner) docOwner).getModifierList() : null;
97 | if (modifierList != null) {
98 | PsiElement parent = modifierList.getParent();
99 | if (parent instanceof PsiDocCommentOwner && parent.getNavigationElement() instanceof PsiDocCommentOwner) {
100 | return ((PsiDocCommentOwner) parent.getNavigationElement()).getDocComment();
101 | }
102 | }
103 | }
104 | return comment;
105 | }
106 | }
107 |
--------------------------------------------------------------------------------