• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

TAKETODAY / today-infrastructure / 8308118446

16 Mar 2024 01:26PM UTC coverage: 78.393% (+0.02%) from 78.374%
8308118446

push

github

TAKETODAY
:white_check_mark:

63592 of 86119 branches covered (73.84%)

Branch coverage included in aggregate %.

157000 of 195273 relevant lines covered (80.4%)

3.41 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.42
today-context/src/main/java/cn/taketoday/context/annotation/ConfigurationClassParser.java
1
/*
2
 * Copyright 2017 - 2024 the original author or authors.
3
 *
4
 * This program is free software: you can redistribute it and/or modify
5
 * it under the terms of the GNU General Public License as published by
6
 * the Free Software Foundation, either version 3 of the License, or
7
 * (at your option) any later version.
8
 *
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 * GNU General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see [https://www.gnu.org/licenses/]
16
 */
17

18
package cn.taketoday.context.annotation;
19

20
import java.io.IOException;
21
import java.lang.annotation.Annotation;
22
import java.util.ArrayList;
23
import java.util.Collection;
24
import java.util.Collections;
25
import java.util.Comparator;
26
import java.util.Deque;
27
import java.util.HashMap;
28
import java.util.Iterator;
29
import java.util.LinkedHashMap;
30
import java.util.LinkedHashSet;
31
import java.util.List;
32
import java.util.Map;
33
import java.util.Optional;
34
import java.util.Set;
35
import java.util.function.Predicate;
36

37
import cn.taketoday.beans.factory.BeanDefinitionStoreException;
38
import cn.taketoday.beans.factory.annotation.AnnotatedBeanDefinition;
39
import cn.taketoday.beans.factory.config.BeanDefinition;
40
import cn.taketoday.beans.factory.config.BeanDefinitionHolder;
41
import cn.taketoday.beans.factory.parsing.Location;
42
import cn.taketoday.beans.factory.parsing.Problem;
43
import cn.taketoday.beans.factory.parsing.ProblemReporter;
44
import cn.taketoday.beans.factory.support.AbstractBeanDefinition;
45
import cn.taketoday.beans.factory.support.BeanDefinitionReader;
46
import cn.taketoday.context.ApplicationContextException;
47
import cn.taketoday.context.BootstrapContext;
48
import cn.taketoday.context.annotation.ConfigurationCondition.ConfigurationPhase;
49
import cn.taketoday.context.annotation.DeferredImportSelector.Group;
50
import cn.taketoday.core.OrderComparator;
51
import cn.taketoday.core.Ordered;
52
import cn.taketoday.core.annotation.AnnotationAwareOrderComparator;
53
import cn.taketoday.core.annotation.AnnotationUtils;
54
import cn.taketoday.core.annotation.MergedAnnotation;
55
import cn.taketoday.core.annotation.MergedAnnotations;
56
import cn.taketoday.core.env.ConfigurableEnvironment;
57
import cn.taketoday.core.env.Environment;
58
import cn.taketoday.core.io.PropertySourceDescriptor;
59
import cn.taketoday.core.io.PropertySourceProcessor;
60
import cn.taketoday.core.type.AnnotationMetadata;
61
import cn.taketoday.core.type.MethodMetadata;
62
import cn.taketoday.core.type.StandardAnnotationMetadata;
63
import cn.taketoday.core.type.classreading.MetadataReader;
64
import cn.taketoday.core.type.filter.AssignableTypeFilter;
65
import cn.taketoday.lang.Assert;
66
import cn.taketoday.lang.Nullable;
67
import cn.taketoday.logging.Logger;
68
import cn.taketoday.logging.LoggerFactory;
69
import cn.taketoday.stereotype.Component;
70
import cn.taketoday.util.ClassUtils;
71
import cn.taketoday.util.LinkedMultiValueMap;
72
import cn.taketoday.util.MultiValueMap;
73
import cn.taketoday.util.StringUtils;
74

75
/**
76
 * Parses a {@link Configuration} class definition, populating a collection of
77
 * {@link ConfigurationClass} objects (parsing a single Configuration class may result in
78
 * any number of ConfigurationClass objects because one Configuration class may import
79
 * another using the {@link Import} annotation).
80
 *
81
 * <p>This class helps separate the concern of parsing the structure of a Configuration
82
 * class from the concern of registering BeanDefinition objects based on the content of
83
 * that model (with the exception of {@code @ComponentScan} annotations which need to be
84
 * registered immediately).
85
 *
86
 * <p>This ASM-based implementation avoids reflection and eager class loading in order to
87
 * interoperate effectively with lazy class loading in a Framework ApplicationContext.
88
 *
89
 * @author Chris Beams
90
 * @author Juergen Hoeller
91
 * @author Phillip Webb
92
 * @author Sam Brannen
93
 * @author Stephane Nicoll
94
 * @author <a href="https://github.com/TAKETODAY">Harry Yang</a>
95
 * @see ConfigurationClassBeanDefinitionReader
96
 * @since 4.0
97
 */
98
class ConfigurationClassParser {
99

100
  private static final Predicate<String> DEFAULT_EXCLUSION_FILTER = className ->
2✔
101
          className.startsWith("java.lang.annotation.")
7!
102
                  || className.startsWith("cn.taketoday.lang.");
3!
103

104
  private static final Predicate<Condition> REGISTER_BEAN_CONDITION_FILTER = condition ->
2✔
105
          condition instanceof ConfigurationCondition configCondition
5✔
106
                  && ConfigurationPhase.REGISTER_BEAN.equals(configCondition.getConfigurationPhase());
9!
107

108
  private static final Comparator<DeferredImportSelectorHolder> DEFERRED_IMPORT_COMPARATOR =
3✔
109
          (o1, o2) -> AnnotationAwareOrderComparator.INSTANCE.compare(o1.importSelector, o2.importSelector);
7✔
110

111
  private final Logger logger = LoggerFactory.getLogger(getClass());
5✔
112

113
  private final ComponentScanAnnotationParser componentScanParser;
114

115
  private final LinkedHashMap<ConfigurationClass, ConfigurationClass> configurationClasses = new LinkedHashMap<>();
5✔
116

117
  private final MultiValueMap<String, ConfigurationClass> knownSuperclasses = new LinkedMultiValueMap<>();
5✔
118

119
  private final DeferredImportSelectorHandler deferredImportSelectorHandler = new DeferredImportSelectorHandler();
6✔
120

121
  private final SourceClass objectSourceClass = new SourceClass(Object.class);
7✔
122

123
  private final BootstrapContext bootstrapContext;
124

125
  @Nullable
126
  private final PropertySourceRegistry propertySourceRegistry;
127

128
  public final ImportRegistry importRegistry = new ImportStack();
6✔
129

130
  /**
131
   * Create a new {@link ConfigurationClassParser} instance that will be used
132
   * to populate the set of configuration classes.
133
   */
134
  public ConfigurationClassParser(BootstrapContext bootstrapContext) {
2✔
135
    this.bootstrapContext = bootstrapContext;
3✔
136
    this.componentScanParser = new ComponentScanAnnotationParser(bootstrapContext);
6✔
137
    this.propertySourceRegistry = bootstrapContext.getEnvironment() instanceof ConfigurableEnvironment ce ?
10!
138
            new PropertySourceRegistry(new PropertySourceProcessor(ce, bootstrapContext.getResourceLoader())) : null;
11✔
139
  }
1✔
140

141
  public void parse(Set<BeanDefinitionHolder> configCandidates) {
142
    for (BeanDefinitionHolder holder : configCandidates) {
10✔
143
      BeanDefinition bd = holder.getBeanDefinition();
3✔
144
      try {
145
        if (bd instanceof AnnotatedBeanDefinition) {
3✔
146
          parse(((AnnotatedBeanDefinition) bd), holder.getBeanName());
7✔
147
        }
148
        else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
7!
149
          parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
8✔
150
        }
151
        else {
152
          parse(bd.getBeanClassName(), holder.getBeanName());
6✔
153
        }
154
      }
155
      catch (BeanDefinitionStoreException ex) {
1✔
156
        throw ex;
2✔
157
      }
158
      catch (Throwable ex) {
1✔
159
        throw new BeanDefinitionStoreException(
3✔
160
                "Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
5✔
161
      }
1✔
162
    }
1✔
163

164
    deferredImportSelectorHandler.process();
3✔
165
  }
1✔
166

167
  private void parse(AnnotatedBeanDefinition beanDef, String beanName) {
168
    processConfigurationClass(new ConfigurationClass(
5✔
169
            beanDef.getMetadata(), beanName, (beanDef instanceof ScannedGenericBeanDefinition)), DEFAULT_EXCLUSION_FILTER);
6✔
170
  }
1✔
171

172
  private void parse(Class<?> clazz, String beanName) {
173
    processConfigurationClass(new ConfigurationClass(clazz, beanName), DEFAULT_EXCLUSION_FILTER);
8✔
174
  }
1✔
175

176
  final void parse(@Nullable String className, String beanName) throws IOException {
177
    Assert.notNull(className, "No bean class name for configuration class bean definition");
3✔
178
    MetadataReader reader = bootstrapContext.getMetadataReader(className);
5✔
179
    processConfigurationClass(new ConfigurationClass(reader, beanName), DEFAULT_EXCLUSION_FILTER);
8✔
180
  }
1✔
181

182
  /**
183
   * Validate each {@link ConfigurationClass} object.
184
   *
185
   * @see ConfigurationClass#validate
186
   */
187
  public void validate() {
188
    ProblemReporter problemReporter = bootstrapContext.getProblemReporter();
4✔
189
    for (ConfigurationClass configClass : configurationClasses.keySet()) {
12✔
190
      configClass.validate(problemReporter);
3✔
191
    }
1✔
192
  }
1✔
193

194
  public Set<ConfigurationClass> getConfigurationClasses() {
195
    return configurationClasses.keySet();
4✔
196
  }
197

198
  List<PropertySourceDescriptor> getPropertySourceDescriptors() {
199
    return propertySourceRegistry != null ? propertySourceRegistry.getDescriptors()
8!
200
            : Collections.emptyList();
×
201
  }
202

203
  protected void processConfigurationClass(ConfigurationClass configClass, Predicate<String> filter) {
204
    if (bootstrapContext.passCondition(configClass.metadata, ConfigurationPhase.PARSE_CONFIGURATION)) {
7✔
205
      ConfigurationClass existingClass = configurationClasses.get(configClass);
6✔
206
      if (existingClass != null) {
2✔
207
        if (configClass.isImported()) {
3✔
208
          if (existingClass.isImported()) {
3✔
209
            existingClass.mergeImportedBy(configClass);
3✔
210
          }
211
          // Otherwise ignore new imported config class; existing non-imported class overrides it.
212
          return;
1✔
213
        }
214
        else if (configClass.scanned) {
3✔
215
          String beanName = configClass.beanName;
3✔
216
          if (StringUtils.isNotEmpty(beanName) && bootstrapContext.containsBeanDefinition(beanName)) {
8!
217
            bootstrapContext.removeBeanDefinition(beanName);
4✔
218
          }
219
          // An implicitly scanned bean definition should not override an explicit import.
220
          return;
1✔
221
        }
222
        else {
223
          // Explicit bean definition found, probably replacing an import.
224
          // Let's remove the old one and go with the new one.
225
          configurationClasses.remove(configClass);
5✔
226
          removeKnownSuperclass(configClass.metadata.getClassName(), false);
6✔
227
        }
228
      }
229

230
      // Recursively process the configuration class and its superclass hierarchy.
231
      SourceClass sourceClass = null;
2✔
232
      try {
233
        sourceClass = asSourceClass(configClass, filter);
5✔
234
        do {
235
          sourceClass = doProcessConfigurationClass(configClass, sourceClass, filter);
6✔
236
        }
237
        while (sourceClass != null);
2✔
238
      }
239
      catch (IOException ex) {
1✔
240
        throw new BeanDefinitionStoreException(
7✔
241
                "I/O failure while processing configuration class [" + sourceClass + "]", ex);
242
      }
1✔
243

244
      configurationClasses.put(configClass, configClass);
6✔
245
    }
246
  }
1✔
247

248
  /**
249
   * Apply processing and build a complete {@link ConfigurationClass} by reading the
250
   * annotations, members and methods from the source class. This method can be called
251
   * multiple times as relevant sources are discovered.
252
   *
253
   * @param configClass the configuration class being build
254
   * @param sourceClass a source class
255
   * @return the superclass, or {@code null} if none found or previously processed
256
   */
257
  @Nullable
258
  protected final SourceClass doProcessConfigurationClass(
259
          ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter) throws IOException {
260

261
    if (configClass.metadata.isAnnotated(Component.class.getName())) {
6✔
262
      // Recursively process any member (nested) classes first
263
      processMemberClasses(configClass, sourceClass, filter);
5✔
264
    }
265

266
    // Process any @PropertySource annotations
267
    Environment environment = bootstrapContext.getEnvironment();
4✔
268
    MergedAnnotations annotations = sourceClass.metadata.getAnnotations();
4✔
269
    for (var propertySource : sourceClass.metadata.getMergedRepeatableAnnotation(
15✔
270
            PropertySource.class, PropertySources.class, true)) {
271
      if (this.propertySourceRegistry != null) {
3!
272
        this.propertySourceRegistry.processPropertySource(propertySource);
5✔
273
      }
274
      else {
275
        logger.info("Ignoring @PropertySource annotation on [{}]. Reason: Environment must implement ConfigurableEnvironment",
×
276
                sourceClass.metadata.getClassName());
×
277
      }
278
    }
1✔
279

280
    // Process any @ComponentScan annotations
281

282
    var componentScans = sourceClass.metadata.getMergedRepeatableAnnotation(ComponentScan.class, ComponentScans.class,
8✔
283
            false, MergedAnnotation::isDirectlyPresent);
284

285
    // Fall back to searching for @ComponentScan meta-annotations (which indirectly
286
    // includes locally declared composed annotations).
287
    if (componentScans.isEmpty()) {
3✔
288
      componentScans = sourceClass.metadata.getMergedRepeatableAnnotation(
8✔
289
              ComponentScan.class, ComponentScans.class, false, MergedAnnotation::isMetaPresent);
290
    }
291

292
    if (!componentScans.isEmpty()) {
3✔
293
      List<Condition> registerBeanConditions = collectRegisterBeanConditions(configClass);
4✔
294
      if (!registerBeanConditions.isEmpty()) {
3✔
295
        throw new ApplicationContextException(
6✔
296
                "Component scan could not be used with conditions in REGISTER_BEAN phase: " + registerBeanConditions);
297
      }
298
      for (MergedAnnotation<ComponentScan> componentScan : componentScans) {
10✔
299
        // The config class is annotated with @ComponentScan -> perform the scan immediately
300
        Set<BeanDefinitionHolder> scannedBeanDefinitions =
5✔
301
                componentScanParser.parse(componentScan, sourceClass.metadata.getClassName());
3✔
302
        // Check the set of scanned definitions for any further config classes and parse recursively if needed
303
        for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
10✔
304
          BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
4✔
305
          if (bdCand == null) {
2✔
306
            bdCand = holder.getBeanDefinition();
3✔
307
          }
308
          if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, bootstrapContext)) {
5✔
309
            parse(bdCand.getBeanClassName(), holder.getBeanName());
6✔
310
          }
311
        }
1✔
312
      }
1✔
313
    }
314

315
    // Process any @Import annotations
316
    processImports(configClass, sourceClass, getImports(sourceClass), filter, true);
9✔
317

318
    // Process any @ImportResource annotations
319
    MergedAnnotation<ImportResource> importResource = annotations.get(ImportResource.class);
4✔
320
    if (importResource.isPresent()) {
3✔
321
      String[] resources = importResource.getStringArray("locations");
4✔
322
      Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
4✔
323
      for (String resource : resources) {
16✔
324
        String resolvedResource = environment.resolveRequiredPlaceholders(resource);
4✔
325
        configClass.addImportedResource(resolvedResource, readerClass);
4✔
326
      }
327
    }
328

329
    // Process individual @Component methods
330
    Set<MethodMetadata> beanMethods = retrieveComponentMethodMetadata(sourceClass);
4✔
331
    for (MethodMetadata methodMetadata : beanMethods) {
10✔
332
      configClass.addMethod(new ComponentMethod(methodMetadata, configClass));
7✔
333
    }
1✔
334

335
    // Process default methods on interfaces
336
    processInterfaces(configClass, sourceClass);
4✔
337

338
    // Process superclass, if any
339
    if (sourceClass.metadata.hasSuperClass()) {
4✔
340
      String superclass = sourceClass.metadata.getSuperClassName();
4✔
341
      if (superclass != null && !superclass.startsWith("java")) {
6!
342
        boolean superclassKnown = this.knownSuperclasses.containsKey(superclass);
5✔
343
        this.knownSuperclasses.add(superclass, configClass);
5✔
344
        if (!superclassKnown) {
2✔
345
          // Superclass found, return its annotation metadata and recurse
346
          return sourceClass.getSuperClass();
3✔
347
        }
348
      }
349
    }
350

351
    // No superclass -> processing is complete
352
    return null;
2✔
353
  }
354

355
  /**
356
   * Register member (nested) classes that happen to be configuration classes themselves.
357
   */
358
  private void processMemberClasses(ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
359
          throws IOException //
360
  {
361
    Collection<SourceClass> memberClasses = sourceClass.getMemberClasses();
3✔
362
    if (!memberClasses.isEmpty()) {
3✔
363
      ArrayList<SourceClass> candidates = new ArrayList<>(memberClasses.size());
6✔
364
      for (SourceClass memberClass : memberClasses) {
10✔
365
        if (ConfigurationClassUtils.isConfigurationCandidate(memberClass.metadata)
6✔
366
                && !memberClass.metadata.getClassName().equals(configClass.metadata.getClassName())) {
6✔
367
          candidates.add(memberClass);
4✔
368
        }
369
      }
1✔
370
      OrderComparator.sort(candidates);
2✔
371
      for (SourceClass candidate : candidates) {
10✔
372
        if (importRegistry.contains(configClass)) {
5!
373
          bootstrapContext.reportError(new CircularImportProblem(configClass, importRegistry));
×
374
        }
375
        else {
376
          importRegistry.push(configClass);
4✔
377
          try {
378
            processConfigurationClass(candidate.asConfigClass(configClass), filter);
6✔
379
          }
380
          finally {
381
            importRegistry.pop();
4✔
382
          }
383
        }
384
      }
1✔
385
    }
386
  }
1✔
387

388
  /**
389
   * Register default methods on interfaces implemented by the configuration class.
390
   */
391
  private void processInterfaces(ConfigurationClass configClass, SourceClass sourceClass) throws IOException {
392
    for (SourceClass ifc : sourceClass.getInterfaces()) {
11✔
393
      Set<MethodMetadata> beanMethods = retrieveComponentMethodMetadata(ifc);
4✔
394
      for (MethodMetadata methodMetadata : beanMethods) {
10✔
395
        if (!methodMetadata.isAbstract()) {
3!
396
          // A default method or other concrete method on a Java 8+ interface...
397
          configClass.addMethod(new ComponentMethod(methodMetadata, configClass));
7✔
398
        }
399
      }
1✔
400
      processInterfaces(configClass, ifc);
4✔
401
    }
1✔
402
  }
1✔
403

404
  /**
405
   * Retrieve the metadata for all <code>@Component</code> methods.
406
   */
407
  private Set<MethodMetadata> retrieveComponentMethodMetadata(SourceClass sourceClass) {
408
    AnnotationMetadata original = sourceClass.metadata;
3✔
409
    Set<MethodMetadata> componentMethods = original.getAnnotatedMethods(Component.class.getName());
5✔
410
    if (componentMethods.size() > 1 && original instanceof StandardAnnotationMetadata) {
7✔
411
      // Try reading the class file via ASM for deterministic declaration order...
412
      // Unfortunately, the JVM's standard reflection returns methods in arbitrary
413
      // order, even between different runs of the same application on the same JVM.
414
      try {
415
        AnnotationMetadata asm = bootstrapContext.getAnnotationMetadata(original.getClassName());
6✔
416
        Set<MethodMetadata> asmMethods = asm.getAnnotatedMethods(Component.class.getName());
5✔
417
        if (asmMethods.size() >= componentMethods.size()) {
5✔
418
          LinkedHashSet<MethodMetadata> candidateMethods = new LinkedHashSet<>(componentMethods);
5✔
419
          LinkedHashSet<MethodMetadata> selectedMethods = new LinkedHashSet<>(asmMethods.size());
6✔
420
          for (MethodMetadata asmMethod : asmMethods) {
10✔
421
            for (Iterator<MethodMetadata> it = candidateMethods.iterator(); it.hasNext(); ) {
6!
422
              MethodMetadata beanMethod = it.next();
4✔
423
              if (beanMethod.getMethodName().equals(asmMethod.getMethodName())) {
6✔
424
                selectedMethods.add(beanMethod);
4✔
425
                it.remove();
2✔
426
                break;
1✔
427
              }
428
            }
1✔
429
          }
1✔
430
          if (selectedMethods.size() == componentMethods.size()) {
5!
431
            // All reflection-detected methods found in ASM method set -> proceed
432
            componentMethods = selectedMethods;
2✔
433
          }
434
        }
435
      }
436
      catch (Exception ex) {
×
437
        logger.debug("Failed to read class file via ASM for determining @Component method order", ex);
×
438
        // No worries, let's continue with the reflection metadata we started with...
439
      }
1✔
440
    }
441
    return componentMethods;
2✔
442
  }
443

444
  /**
445
   * Remove known superclasses for the given removed class, potentially replacing
446
   * the superclass exposure on a different config class with the same superclass.
447
   */
448
  private void removeKnownSuperclass(String removedClass, boolean replace) {
449
    String replacedSuperclass = null;
2✔
450
    ConfigurationClass replacingClass = null;
2✔
451

452
    var it = this.knownSuperclasses.entrySet().iterator();
5✔
453
    while (it.hasNext()) {
3✔
454
      Map.Entry<String, List<ConfigurationClass>> entry = it.next();
4✔
455
      if (entry.getValue().removeIf(configClass -> configClass.metadata.getClassName().equals(removedClass))) {
13✔
456
        if (entry.getValue().isEmpty()) {
5✔
457
          it.remove();
3✔
458
        }
459
        else if (replace && replacingClass == null) {
4!
460
          replacedSuperclass = entry.getKey();
4✔
461
          replacingClass = entry.getValue().get(0);
7✔
462
        }
463
      }
464
    }
1✔
465

466
    if (replacingClass != null) {
2✔
467
      try {
468
        SourceClass sourceClass = asSourceClass(replacingClass, DEFAULT_EXCLUSION_FILTER).getSuperClass();
6✔
469
        while (!sourceClass.metadata.getClassName().equals(replacedSuperclass)
6!
470
                && sourceClass.metadata.getSuperClassName() != null) {
×
471
          sourceClass = sourceClass.getSuperClass();
×
472
        }
473
        do {
474
          sourceClass = doProcessConfigurationClass(replacingClass, sourceClass, DEFAULT_EXCLUSION_FILTER);
6✔
475
        }
476
        while (sourceClass != null);
2✔
477
      }
478
      catch (IOException ex) {
×
479
        throw new BeanDefinitionStoreException(
×
480
                "I/O failure while removing configuration class [%s]".formatted(removedClass), ex);
×
481
      }
1✔
482
    }
483
  }
1✔
484

485
  /**
486
   * Returns {@code @Import} class, considering all meta-annotations.
487
   */
488
  private Set<SourceClass> getImports(SourceClass sourceClass) throws IOException {
489
    Set<SourceClass> imports = new LinkedHashSet<>();
4✔
490
    Set<SourceClass> visited = new LinkedHashSet<>();
4✔
491
    collectImports(sourceClass, imports, visited);
5✔
492
    return imports;
2✔
493
  }
494

495
  /**
496
   * Recursively collect all declared {@code @Import} values. Unlike most
497
   * meta-annotations it is valid to have several {@code @Import}s declared with
498
   * different values; the usual process of returning values from the first
499
   * meta-annotation on a class is not sufficient.
500
   * <p>For example, it is common for a {@code @Configuration} class to declare direct
501
   * {@code @Import}s in addition to meta-imports originating from an {@code @Enable}
502
   * annotation.
503
   *
504
   * @param sourceClass the class to search
505
   * @param imports the imports collected so far
506
   * @param visited used to track visited classes to prevent infinite recursion
507
   * @throws IOException if there is any problem reading metadata from the named class
508
   */
509
  private void collectImports(SourceClass sourceClass, Set<SourceClass> imports, Set<SourceClass> visited)
510
          throws IOException //
511
  {
512
    if (visited.add(sourceClass)) {
4✔
513
      for (SourceClass annotation : sourceClass.getAnnotationsAsSourceClass()) {
11✔
514
        String annName = annotation.metadata.getClassName();
4✔
515
        if (!annName.equals(Import.class.getName())) {
5✔
516
          collectImports(annotation, imports, visited);
5✔
517
        }
518
      }
1✔
519
      imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value"));
8✔
520
    }
521
  }
1✔
522

523
  private void processImports(ConfigurationClass configClass,
524
          SourceClass currentSourceClass, Collection<SourceClass> importCandidates,
525
          Predicate<String> exclusionFilter, boolean checkForCircularImports) //
526
  {
527
    if (importCandidates.isEmpty()) {
3✔
528
      return;
1✔
529
    }
530

531
    if (checkForCircularImports && isChainedImportOnStack(configClass)) {
6✔
532
      bootstrapContext.reportError(new CircularImportProblem(configClass, importRegistry));
×
533
    }
534
    else {
535
      importRegistry.push(configClass);
4✔
536
      try {
537
        for (SourceClass candidate : importCandidates) {
10✔
538
          if (candidate.isAssignable(ImportSelector.class)) {
4✔
539
            // Candidate class is an ImportSelector -> delegate to it to determine imports
540
            Class<?> candidateClass = candidate.loadClass();
3✔
541
            var selector = bootstrapContext.instantiate(candidateClass, ImportSelector.class);
7✔
542
            Predicate<String> selectorFilter = selector.getExclusionFilter();
3✔
543
            if (selectorFilter != null) {
2✔
544
              exclusionFilter = exclusionFilter.or(selectorFilter);
4✔
545
            }
546
            if (selector instanceof DeferredImportSelector deferredSelector) {
6✔
547
              deferredImportSelectorHandler.handle(configClass, deferredSelector);
6✔
548
            }
549
            else {
550
              String[] importClassNames = selector.selectImports(currentSourceClass.metadata);
5✔
551
              Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames, exclusionFilter);
5✔
552
              processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter, false);
7✔
553
            }
554
          }
1✔
555
          else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
4✔
556
            // Candidate class is an ImportBeanDefinitionRegistrar ->
557
            // delegate to it to register additional bean definitions
558
            Class<?> candidateClass = candidate.loadClass();
3✔
559
            var registrar = bootstrapContext.instantiate(candidateClass, ImportBeanDefinitionRegistrar.class);
7✔
560
            configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.metadata);
5✔
561
          }
1✔
562
          else {
563
            // Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
564
            // process it as an @Configuration class
565
            importRegistry.registerImport(
7✔
566
                    currentSourceClass.metadata, candidate.metadata.getClassName());
1✔
567
            processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter);
6✔
568
          }
569
        }
1✔
570
      }
571
      catch (BeanDefinitionStoreException ex) {
1✔
572
        throw ex;
2✔
573
      }
574
      catch (Throwable ex) {
1✔
575
        throw new BeanDefinitionStoreException(
4✔
576
                "Failed to process import candidates for configuration class [" +
577
                        configClass.metadata.getClassName() + "]: " + ex.getMessage(), ex);
7✔
578
      }
579
      finally {
580
        importRegistry.pop();
4✔
581
      }
582
    }
583
  }
1✔
584

585
  private boolean isChainedImportOnStack(ConfigurationClass configClass) {
586
    if (importRegistry.contains(configClass)) {
5✔
587
      String configClassName = configClass.metadata.getClassName();
4✔
588
      AnnotationMetadata importingClass = importRegistry.getImportingClassFor(configClassName);
5✔
589
      while (importingClass != null) {
2✔
590
        if (configClassName.equals(importingClass.getClassName())) {
5✔
591
          return true;
2✔
592
        }
593
        importingClass = importRegistry.getImportingClassFor(importingClass.getClassName());
7✔
594
      }
595
    }
596
    return false;
2✔
597
  }
598

599
  /**
600
   * Factory method to obtain a {@link SourceClass} from a {@link ConfigurationClass}.
601
   */
602
  private SourceClass asSourceClass(ConfigurationClass configurationClass, Predicate<String> filter) throws IOException {
603
    AnnotationMetadata metadata = configurationClass.metadata;
3✔
604
    if (metadata instanceof StandardAnnotationMetadata) {
3✔
605
      return asSourceClass(((StandardAnnotationMetadata) metadata).getIntrospectedClass(), filter);
7✔
606
    }
607
    return asSourceClass(metadata.getClassName(), filter);
6✔
608
  }
609

610
  /**
611
   * Factory method to obtain a {@link SourceClass} from a {@link Class}.
612
   */
613
  SourceClass asSourceClass(@Nullable Class<?> classType, Predicate<String> filter) throws IOException {
614
    if (classType == null || filter.test(classType.getName())) {
7!
615
      return objectSourceClass;
×
616
    }
617
    try {
618
      // Sanity test that we can reflectively read annotations,
619
      // including Class attributes; if not -> fall back to ASM
620
      for (Annotation ann : classType.getDeclaredAnnotations()) {
17✔
621
        AnnotationUtils.validateAnnotation(ann);
2✔
622
      }
623
      return new SourceClass(classType);
6✔
624
    }
625
    catch (Throwable ex) {
1✔
626
      // Enforce ASM via class name resolution
627
      return asSourceClass(classType.getName(), filter);
6✔
628
    }
629
  }
630

631
  /**
632
   * Factory method to obtain a {@link SourceClass} collection from class names.
633
   */
634
  private Collection<SourceClass> asSourceClasses(String[] classNames, Predicate<String> filter) throws IOException {
635
    ArrayList<SourceClass> annotatedClasses = new ArrayList<>(classNames.length);
6✔
636
    for (String className : classNames) {
16✔
637
      annotatedClasses.add(asSourceClass(className, filter));
7✔
638
    }
639
    return annotatedClasses;
2✔
640
  }
641

642
  /**
643
   * Factory method to obtain a {@link SourceClass} from a class name.
644
   */
645
  SourceClass asSourceClass(@Nullable String className, Predicate<String> filter) throws IOException {
646
    if (className == null || filter.test(className)) {
6!
647
      return objectSourceClass;
3✔
648
    }
649
    if (className.startsWith("java")) {
4✔
650
      // Never use ASM for core java types
651
      try {
652
        return new SourceClass(ClassUtils.forName(className, bootstrapContext.getClassLoader()));
10✔
653
      }
654
      catch (ClassNotFoundException ex) {
×
655
        throw new IOException("Failed to load class [" + className + "]", ex);
×
656
      }
657
    }
658
    return new SourceClass(bootstrapContext.getMetadataReader(className));
9✔
659
  }
660

661
  private List<Condition> collectRegisterBeanConditions(ConfigurationClass configurationClass) {
662
    AnnotationMetadata metadata = configurationClass.metadata;
3✔
663
    ConditionEvaluator conditionEvaluator = bootstrapContext.getConditionEvaluator();
4✔
664
    ArrayList<Condition> allConditions = new ArrayList<>(conditionEvaluator.collectConditions(metadata));
7✔
665
    ConfigurationClass enclosingConfigurationClass = getEnclosingConfigurationClass(configurationClass);
4✔
666
    if (enclosingConfigurationClass != null) {
2✔
667
      allConditions.addAll(conditionEvaluator.collectConditions(enclosingConfigurationClass.metadata));
7✔
668
    }
669
    return allConditions.stream().filter(REGISTER_BEAN_CONDITION_FILTER).toList();
6✔
670
  }
671

672
  @Nullable
673
  private ConfigurationClass getEnclosingConfigurationClass(ConfigurationClass configurationClass) {
674
    String enclosingClassName = configurationClass.metadata.getEnclosingClassName();
4✔
675
    if (enclosingClassName != null) {
2✔
676
      return configurationClass.importedBy.stream()
6✔
677
              .filter(candidate -> enclosingClassName.equals(candidate.metadata.getClassName()))
7✔
678
              .findFirst()
2✔
679
              .orElse(null);
2✔
680
    }
681
    return null;
2✔
682
  }
683

684
  private class DeferredImportSelectorHandler {
5✔
685

686
    @Nullable
6✔
687
    private ArrayList<DeferredImportSelectorHolder> deferredImportSelectors = new ArrayList<>();
688

689
    /**
690
     * Handle the specified {@link DeferredImportSelector}. If deferred import
691
     * selectors are being collected, this registers this instance to the list. If
692
     * they are being processed, the {@link DeferredImportSelector} is also processed
693
     * immediately according to its {@link DeferredImportSelector.Group}.
694
     *
695
     * @param configClass the source configuration class
696
     * @param importSelector the selector to handle
697
     */
698
    public void handle(ConfigurationClass configClass, DeferredImportSelector importSelector) {
699
      DeferredImportSelectorHolder holder = new DeferredImportSelectorHolder(configClass, importSelector);
6✔
700
      if (deferredImportSelectors == null) {
3✔
701
        DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
6✔
702
        handler.register(holder);
3✔
703
        handler.processGroupImports();
2✔
704
      }
1✔
705
      else {
706
        deferredImportSelectors.add(holder);
5✔
707
      }
708
    }
1✔
709

710
    public void process() {
711
      List<DeferredImportSelectorHolder> deferredImports = deferredImportSelectors;
3✔
712
      this.deferredImportSelectors = null;
3✔
713
      try {
714
        if (deferredImports != null) {
2!
715
          var handler = new DeferredImportSelectorGroupingHandler();
6✔
716
          deferredImports.sort(DEFERRED_IMPORT_COMPARATOR);
3✔
717

718
          for (DeferredImportSelectorHolder deferredImport : deferredImports) {
10✔
719
            handler.register(deferredImport);
3✔
720
          }
1✔
721

722
          handler.processGroupImports();
2✔
723
        }
724
      }
725
      finally {
726
        this.deferredImportSelectors = new ArrayList<>();
5✔
727
      }
728
    }
1✔
729
  }
730

731
  private class DeferredImportSelectorGroupingHandler {
5✔
732

733
    private final HashMap<AnnotationMetadata, ConfigurationClass> configurationClasses = new HashMap<>();
5✔
734
    private final LinkedHashMap<Object, DeferredImportSelectorGrouping> groupings = new LinkedHashMap<>();
6✔
735

736
    public void register(DeferredImportSelectorHolder deferredImport) {
737
      var group = deferredImport.importSelector.getImportGroup();
4✔
738
      var grouping = groupings.computeIfAbsent(
5✔
739
              group != null ? group : deferredImport,
8✔
740
              key -> new DeferredImportSelectorGrouping(createGroup(group))
7✔
741
      );
742
      grouping.add(deferredImport);
3✔
743
      configurationClasses.put(deferredImport.configurationClass.metadata,
9✔
744
              deferredImport.configurationClass);
745
    }
1✔
746

747
    public void processGroupImports() {
748
      for (DeferredImportSelectorGrouping grouping : groupings.values()) {
12✔
749
        Predicate<String> exclusionFilter = grouping.getCandidateFilter();
3✔
750
        for (Group.Entry entry : grouping.getImports()) {
11✔
751
          ConfigurationClass configurationClass = configurationClasses.get(entry.metadata());
7✔
752
          try {
753
            processImports(configurationClass, asSourceClass(configurationClass, exclusionFilter),
12✔
754
                    Collections.singleton(asSourceClass(entry.importClassName(), exclusionFilter)),
6✔
755
                    exclusionFilter, false);
756
          }
757
          catch (BeanDefinitionStoreException ex) {
×
758
            throw ex;
×
759
          }
760
          catch (Throwable ex) {
×
761
            throw new BeanDefinitionStoreException(
×
762
                    "Failed to process import candidates for configuration class [" +
763
                            configurationClass.metadata.getClassName() + "]", ex);
×
764
          }
1✔
765
        }
1✔
766
      }
1✔
767
    }
1✔
768

769
    private Group createGroup(@Nullable Class<? extends Group> type) {
770
      Class<? extends Group> effectiveType = type != null ? type : DefaultDeferredImportSelectorGroup.class;
6✔
771
      return bootstrapContext.instantiate(effectiveType, Group.class);
8✔
772
    }
773
  }
774

775
  private static class DeferredImportSelectorHolder {
776

777
    public final ConfigurationClass configurationClass;
778
    public final DeferredImportSelector importSelector;
779

780
    public DeferredImportSelectorHolder(ConfigurationClass configClass, DeferredImportSelector selector) {
2✔
781
      this.configurationClass = configClass;
3✔
782
      this.importSelector = selector;
3✔
783
    }
1✔
784
  }
785

786
  private static class DeferredImportSelectorGrouping {
787

788
    private final DeferredImportSelector.Group group;
789
    private final List<DeferredImportSelectorHolder> deferredImports = new ArrayList<>();
5✔
790

791
    DeferredImportSelectorGrouping(Group group) {
2✔
792
      this.group = group;
3✔
793
    }
1✔
794

795
    public void add(DeferredImportSelectorHolder deferredImport) {
796
      this.deferredImports.add(deferredImport);
5✔
797
    }
1✔
798

799
    /**
800
     * Return the imports defined by the group.
801
     *
802
     * @return each import with its associated configuration class
803
     */
804
    public Iterable<Group.Entry> getImports() {
805
      for (DeferredImportSelectorHolder deferredImport : deferredImports) {
11✔
806
        group.process(deferredImport.configurationClass.metadata, deferredImport.importSelector);
8✔
807
      }
1✔
808
      return group.selectImports();
4✔
809
    }
810

811
    public Predicate<String> getCandidateFilter() {
812
      Predicate<String> mergedFilter = DEFAULT_EXCLUSION_FILTER;
2✔
813
      for (DeferredImportSelectorHolder deferredImport : deferredImports) {
11✔
814
        Predicate<String> selectorFilter = deferredImport.importSelector.getExclusionFilter();
4✔
815
        if (selectorFilter != null) {
2✔
816
          mergedFilter = mergedFilter.or(selectorFilter);
4✔
817
        }
818
      }
1✔
819
      return mergedFilter;
2✔
820
    }
821
  }
822

823
  private static class DefaultDeferredImportSelectorGroup implements Group {
2✔
824

825
    private final ArrayList<Entry> imports = new ArrayList<>();
6✔
826

827
    @Override
828
    public void process(AnnotationMetadata metadata, DeferredImportSelector selector) {
829
      for (String importClassName : selector.selectImports(metadata)) {
18✔
830
        imports.add(new Entry(metadata, importClassName));
9✔
831
      }
832
    }
1✔
833

834
    @Override
835
    public Iterable<Entry> selectImports() {
836
      return imports;
3✔
837
    }
838
  }
839

840
  /**
841
   * Simple wrapper that allows annotated source classes to be dealt with
842
   * in a uniform manner, regardless of how they are loaded.
843
   */
844
  private class SourceClass implements Ordered {
845

846
    public final Object source;  // Class or MetadataReader
847
    public final AnnotationMetadata metadata;
848

849
    public SourceClass(Object source) {
5✔
850
      this.source = source;
3✔
851
      if (source instanceof Class) {
3✔
852
        this.metadata = AnnotationMetadata.introspect((Class<?>) source);
6✔
853
      }
854
      else {
855
        this.metadata = ((MetadataReader) source).getAnnotationMetadata();
5✔
856
      }
857
    }
1✔
858

859
    @Override
860
    public int getOrder() {
861
      Integer order = ConfigurationClassUtils.getOrder(metadata);
4✔
862
      return (order != null ? order : Ordered.LOWEST_PRECEDENCE);
7✔
863
    }
864

865
    public Class<?> loadClass() throws ClassNotFoundException {
866
      if (source instanceof Class) {
4✔
867
        return (Class<?>) source;
4✔
868
      }
869
      String className = ((MetadataReader) source).getClassMetadata().getClassName();
6✔
870
      return ClassUtils.forName(className, bootstrapContext.getClassLoader());
7✔
871
    }
872

873
    public boolean isAssignable(Class<?> clazz) throws IOException {
874
      if (source instanceof Class) {
4✔
875
        return clazz.isAssignableFrom((Class<?>) source);
6✔
876
      }
877
      return new AssignableTypeFilter(clazz)
11✔
878
              .match((MetadataReader) source, bootstrapContext.getMetadataReaderFactory());
2✔
879
    }
880

881
    public ConfigurationClass asConfigClass(ConfigurationClass importedBy) {
882
      if (source instanceof Class) {
4✔
883
        return new ConfigurationClass((Class<?>) source, importedBy);
8✔
884
      }
885
      return new ConfigurationClass((MetadataReader) source, importedBy);
8✔
886
    }
887

888
    public Collection<SourceClass> getMemberClasses() throws IOException {
889
      Object sourceToProcess = this.source;
3✔
890
      if (sourceToProcess instanceof Class<?> sourceClass) {
6✔
891
        try {
892
          Class<?>[] declaredClasses = sourceClass.getDeclaredClasses();
3✔
893
          ArrayList<SourceClass> members = new ArrayList<>(declaredClasses.length);
6✔
894
          for (Class<?> declaredClass : declaredClasses) {
16✔
895
            members.add(asSourceClass(declaredClass, DEFAULT_EXCLUSION_FILTER));
8✔
896
          }
897
          return members;
2✔
898
        }
899
        catch (NoClassDefFoundError err) {
×
900
          // getDeclaredClasses() failed because of non-resolvable dependencies
901
          // -> fall back to ASM below
902
          sourceToProcess = bootstrapContext.getMetadataReader(sourceClass.getName());
×
903
        }
904
      }
905

906
      // ASM-based resolution - safe for non-resolvable classes as well
907
      MetadataReader sourceReader = (MetadataReader) sourceToProcess;
3✔
908
      String[] memberClassNames = sourceReader.getClassMetadata().getMemberClassNames();
4✔
909
      ArrayList<SourceClass> members = new ArrayList<>(memberClassNames.length);
6✔
910
      for (String memberClassName : memberClassNames) {
16✔
911
        try {
912
          members.add(asSourceClass(memberClassName, DEFAULT_EXCLUSION_FILTER));
8✔
913
        }
914
        catch (IOException ex) {
×
915
          // Let's skip it if it's not resolvable - we're just looking for candidates
916
          logger.debug("Failed to resolve member class [{}] - not considering it as a configuration class candidate",
×
917
                  memberClassName);
918
        }
1✔
919
      }
920
      return members;
2✔
921
    }
922

923
    public SourceClass getSuperClass() throws IOException {
924
      if (source instanceof Class) {
4✔
925
        return asSourceClass(((Class<?>) source).getSuperclass(), DEFAULT_EXCLUSION_FILTER);
9✔
926
      }
927
      return asSourceClass(
7✔
928
              ((MetadataReader) source).getClassMetadata().getSuperClassName(), DEFAULT_EXCLUSION_FILTER);
3✔
929
    }
930

931
    public LinkedHashSet<SourceClass> getInterfaces() throws IOException {
932
      LinkedHashSet<SourceClass> result = new LinkedHashSet<>();
4✔
933
      if (source instanceof Class<?> sourceClass) {
9✔
934
        for (Class<?> ifcClass : sourceClass.getInterfaces()) {
18✔
935
          result.add(asSourceClass(ifcClass, DEFAULT_EXCLUSION_FILTER));
8✔
936
        }
937
      }
938
      else {
939
        for (String className : metadata.getInterfaceNames()) {
18✔
940
          result.add(asSourceClass(className, DEFAULT_EXCLUSION_FILTER));
8✔
941
        }
942
      }
943
      return result;
2✔
944
    }
945

946
    public Set<SourceClass> getAnnotationsAsSourceClass() {
947
      LinkedHashSet<SourceClass> result = new LinkedHashSet<>();
4✔
948
      if (source instanceof Class<?> sourceClass) {
9✔
949
        for (Annotation ann : sourceClass.getDeclaredAnnotations()) {
18✔
950
          Class<?> annType = ann.annotationType();
3✔
951
          if (!annType.getName().startsWith("java")) {
5✔
952
            try {
953
              result.add(asSourceClass(annType, DEFAULT_EXCLUSION_FILTER));
8✔
954
            }
955
            catch (Throwable ex) {
×
956
              // An annotation not present on the classpath is being ignored
957
              // by the JVM's class loading -> ignore here as well.
958
            }
1✔
959
          }
960
        }
961
      }
962
      else {
963
        for (String className : metadata.getAnnotationTypes()) {
12✔
964
          if (!className.startsWith("java")) {
4!
965
            try {
966
              result.add(getRelated(className));
6✔
967
            }
968
            catch (Throwable ex) {
×
969
              // An annotation not present on the classpath is being ignored
970
              // by the JVM's class loading -> ignore here as well.
971
            }
1✔
972
          }
973
        }
1✔
974
      }
975
      return result;
2✔
976
    }
977

978
    public Collection<SourceClass> getAnnotationAttributes(String annType, String attribute) throws IOException {
979
      MergedAnnotation<Annotation> annotation = metadata.getAnnotation(annType);
5✔
980
      if (annotation.isPresent()) {
3✔
981
        Optional<String[]> optional = annotation.getValue(attribute, String[].class);
5✔
982
        if (optional.isPresent()) {
3!
983
          String[] classNames = optional.get();
4✔
984
          LinkedHashSet<SourceClass> result = new LinkedHashSet<>();
4✔
985
          for (String className : classNames) {
16✔
986
            result.add(getRelated(className));
6✔
987
          }
988
          return result;
2✔
989
        }
990
      }
991
      return Collections.emptySet();
2✔
992
    }
993

994
    private SourceClass getRelated(String className) throws IOException {
995
      if (source instanceof Class) {
4✔
996
        try {
997
          Class<?> clazz = ClassUtils.forName(className, ((Class<?>) source).getClassLoader());
7✔
998
          return asSourceClass(clazz, DEFAULT_EXCLUSION_FILTER);
6✔
999
        }
1000
        catch (ClassNotFoundException ex) {
×
1001
          // Ignore -> fall back to ASM next, except for core java types.
1002
          if (className.startsWith("java")) {
×
1003
            throw new IOException("Failed to load class [" + className + "]", ex);
×
1004
          }
1005
          return new SourceClass(bootstrapContext.getMetadataReader(className));
×
1006
        }
1007
      }
1008
      return asSourceClass(className, DEFAULT_EXCLUSION_FILTER);
6✔
1009
    }
1010

1011
    @Override
1012
    public boolean equals(@Nullable Object other) {
1013
      return this == other
9!
1014
              || (other instanceof SourceClass && metadata.getClassName().equals(((SourceClass) other).metadata.getClassName()));
9!
1015
    }
1016

1017
    @Override
1018
    public int hashCode() {
1019
      return metadata.getClassName().hashCode();
5✔
1020
    }
1021

1022
    @Override
1023
    public String toString() {
1024
      return metadata.getClassName();
4✔
1025
    }
1026
  }
1027

1028
  /**
1029
   * {@link Problem} registered upon detection of a circular {@link Import}.
1030
   */
1031
  private static class CircularImportProblem extends Problem {
1032

1033
    public CircularImportProblem(ConfigurationClass attemptedImport, Deque<ConfigurationClass> importStack) {
1034
      super(String.format("A circular @Import has been detected: " +
12✔
1035
                              "Illegal attempt by @Configuration class '%s' to import class '%s' as '%s' is " +
1036
                              "already present in the current import stack %s", importStack.element().getSimpleName(),
7✔
1037
                      attemptedImport.getSimpleName(), attemptedImport.getSimpleName(), importStack),
11✔
1038
              new Location(importStack.element().resource, attemptedImport.metadata));
6✔
1039
    }
1✔
1040
  }
1041

1042
  class ImportStack extends ImportRegistry {
6✔
1043

1044
    @Override
1045
    public void removeImportingClass(String importingClass) {
1046
      super.removeImportingClass(importingClass);
3✔
1047
      removeKnownSuperclass(importingClass, true);
5✔
1048
    }
1✔
1049

1050
  }
1051

1052
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc