Feature Envy Case Study

From an Incorrect Move Method to a Validated Ref-Plan

This case comes from Segment.buildClassFile(int) in Apache Commons Compress. Designite reports it as Feature Envy with a score of 241.00 and 17 accesses to ClassBands. At first glance, the method appears to be a direct Move Method candidate. Validation shows otherwise: moving the whole method is not a correct refactoring. All 4/4 AutoRefactor variants pass validation, while 0/4 baseline tools succeed.

241.00 Designite feature-envy score
17 access hits to ClassBands
4/4 AutoRefactor variants pass
0/4 baseline tools succeed

Source snippet

Although the method frequently accesses ClassBands, it is not merely ClassBands data-processing logic. buildClassFile(int) also creates the ClassFile, sets version fields, resolves the constant pool, handles class attributes, and updates the class-file attribute array. In other words, ClassBands is an important data source, but the method's main responsibility remains class-file assembly.

Smell code location: commons-compress/src/main/java/org/apache/commons/compress/harmony/unpack200/Segment.java:146 , method buildClassFile(int).

private ClassFile buildClassFile(final int classNum) {
    final ClassFile classFile = new ClassFile();
    final int[] major = classBands.getClassVersionMajor();
    final int[] minor = classBands.getClassVersionMinor();
    if (major != null) {
        classFile.major = major[classNum];
        classFile.minor = minor[classNum];
    } else {
        classFile.major = header.getDefaultClassMajorVersion();
        classFile.minor = header.getDefaultClassMinorVersion();
    }

    final ClassConstantPool cp = classFile.pool;
    final int fullNameIndexInCpClass = classBands.getClassThisInts()[classNum];
    final String fullName = cpBands.getCpClass()[fullNameIndexInCpClass];

    final List<Attribute> classAttributes =
            classBands.getClassAttributes()[classNum];
    SourceFileAttribute sourceFileAttribute = null;
    for (final Attribute classAttribute : classAttributes) {
        if (classAttribute.isSourceFileAttribute()) {
            sourceFileAttribute = (SourceFileAttribute) classAttribute;
        }
    }

    final AttributeLayout SOURCE_FILE =
            attrDefinitionBands.getAttributeDefinitionMap()
                .getAttributeLayout(AttributeLayout.ATTRIBUTE_SOURCE_FILE,
                    AttributeLayout.CONTEXT_CLASS);
    if (SOURCE_FILE.matches(classBands.getRawClassFlags()[classNum])) {
        sourceFileAttribute =
                new SourceFileAttribute(cpBands.cpUTF8Value(fileName, false));
        classFile.attributes =
                new Attribute[] { (Attribute) cp.add(sourceFileAttribute) };
    }
}

Why each comparison tool fails

This case shows why Feature Envy refactoring cannot be reduced to "find a target class and move the whole method." For Segment.buildClassFile(int), the correct solution keeps class-file assembly in Segment and moves only the ClassBands-related data access logic. The baseline tools therefore fail not only because validation rejects their results, but also because their proposed transformations are structurally wrong.

MM-Assist: conflicting move

The IDEA Move Method conflict path is the key failure.

MM-Assist selects SegmentHeader in the forced conflict-rescue path. This class owns header and default-version metadata; it should not take over class-file assembly. During execution, IDEA exposes private-member, visibility, and dependency access conflicts. If "Refactor Anyway" is selected, the resulting patch still fails the regression tests. MM-Assist therefore reaches a refactoring workflow, but the destination is wrong and the forced patch is not valid.

FETruth: non-actionable target

A target-class prediction is not an executable Ref-Plan.

FETruth predicts ClassConstantPool as the target class. This destination reflects part of the constant-pool signal inside the method, but FETruth still attempts to move the entire buildClassFile(int) method. That would assign class-file assembly to a constant-pool class and introduce reverse dependencies on header, cpBands, attrDefinitionBands, ClassBands, and ClassFile. More importantly, this destination cannot satisfy IDEA Move Method preconditions, so it cannot become an executable refactoring.

SWE-agent: build failed

Free-form editing lacks refactoring precondition control.

SWE-agent uses free-form code editing, but it lacks a stable refactoring boundary. A correct solution should preserve Segment's assembly responsibility and move only the ClassBands-related local logic. A free-form patch can easily change both the assembly flow and the data-access logic, breaking method dependencies. In the observed result, SWE-agent's patch does not preserve buildability, so it fails before behavior and smell-removal validation can accept it.

JDeodorant: no candidate

A fixed heuristic workflow has no viable operation here.

JDeodorant does not produce a viable candidate. Its fixed Feature Envy / Move Method workflow is better suited to single-step method moves, but this case requires a compositional "Extract Method + Move Method + AST cleanup" plan. Because JDeodorant cannot express that fine-grained Ref-Plan, it never reaches an effective application and validation stage.

Why AutoRefactor succeeds

AutoRefactor does not move the whole method. It generates a compositional Ref-Plan: first, Extract Method isolates local logic strongly tied to ClassBands, such as class-version reads, classThisInts index access, class-attribute retrieval, and raw-class-flag checks. Then Move Method relocates the extracted helper logic to a more appropriate location, so ClassBands provides class metadata organized by classNum. Finally, AST-level edits repair call sites, parameters, visibility, and imports. As a result, buildClassFile(int) remains in Segment and continues to own class-file assembly, while the lower-level ClassBands data access is encapsulated and verified by compilation, regression tests, and smell-removal checks.