Skip to content

Latest commit

 

History

History
371 lines (267 loc) · 20.6 KB

File metadata and controls

371 lines (267 loc) · 20.6 KB

API-Finder

The API-Finder is the core analysis component of Fika that identifies uncovered third-party library methods and generates detailed information about how to reach them from public entry points. It performs static analysis using call graph construction, coverage analysis, and source code extraction to provide comprehensive data for reachability scenario generation.

A reachability scenario is a piece of code that calls a third-party library method and sets up the program so that a specific path in the code is executed, eventually reaching and running that library method. To run a reachability scenario on its own, we place it inside a unit testing framework and execute it there. We then use test coverage tools to check that the code path actually runs. It looks similar to a unit test, but the goal is different: instead of checking results, it simply shows that a certain dependency call can be executed. Because of this, a reachability scenario does not include any assertions.

Overview

The API-Finder takes a compiled JAR file, analyzes its bytecode to build a call graph, identifies all third-party method invocations, filters out already-covered methods using JaCoCo reports, finds execution paths from public methods to uncovered third-party calls, and extracts complete source code with contextual information for reachability scenario generation.

Architecture

The API-Finder follows this workflow:

  1. Initialization: Load the project JAR and build a call graph
  2. Entry Point Detection: Identify all public methods as potential reachability scenario entry points
  3. Third-Party Discovery: Find all third-party method calls in the codebase
  4. Coverage Filtering: Filter out already-covered third-party calls using JaCoCo reports
  5. Path Finding: Compute paths from entry points to uncovered third-party methods
  6. Source Extraction: Extract complete source code for all methods along each path
  7. Context Extraction: Collect constructors (and factory methods when needed), field declarations, field-modifying methods, and imports
  8. Output Generation: Write JSON reports for reachability scenario generation

How It Works

1. Third-Party Method Identification

Why: Determining which methods belong to third-party libraries versus project code is non-trivial, especially when dealing with transitive dependencies and package naming conflicts.

Implementation: The discovery process uses multiple strategies:

  • Package Mapping: Uses the preprocessor-generated JSON file that maps package names to Maven coordinates. This allows Fika to identify third-party packages quickly.

  • Ignored Prefixes: Maintains a list of prefixes to exclude (e.g., java., jdk., sun., com.sun.) plus the project's own package name. Any additional package names that should be ignored are loaded from ignored_packages.txt.

  • Call Graph Analysis: Iterates through the entire call graph to find all call edges where the target method belongs to a third-party package. Each (caller, callee) pair is recorded.

The implementation is in MethodExtractor.findAllThirdPartyMethodPairs():

  • First pass: Registers all third-party calls to detect when a single class makes multiple calls to the same third-party method (needed for precise coverage analysis)
  • Second pass: Identifies all uncovered third-party method pairs after filtering

2. Entry Point Detection

Why: Identifying which methods should be considered as potential reachability scenario entry points affects the required complexity from the generated reachability scenarios.

Implementation: Fika identifies all public methods in the project's package as entry points (MethodExtractor.detectEntryPoints()).

The entry point detection uses SootUp's class hierarchy analysis to:

  1. Filter classes by project package name
  2. Extract all public methods from those classes
  3. Use these as the starting points for path finding

3. Coverage Measurement and JaCoCo Report Parsing

Why: Accurately determining whether a specific third-party method call is already covered by existing tests is complex. The same third-party method might be called from multiple locations in a class, and we need to know if the specific call site is covered, not just if the method has been executed somewhere.

Implementation: The coverage filtering (CoverageFilter) uses a two-level caching strategy with both HTML and XML report parsing:

Simple Coverage Check (Single Call Site)

When a class has only one call site to a particular third-party method:

  • Parse the JaCoCo HTML report for the class
  • Look for lines containing the third-party method call
  • If any such line is marked as covered (green), the call is considered covered

Precise Coverage Check (Multiple Call Sites)

When a class calls the same third-party method multiple times, we need to know which specific call site is covered:

  1. HTML Analysis (getTargetCallLines()):
    • Parse the HTML to find all line numbers where the target method is invoked
  • Handle special cases like constructors (new ClassName()), child-to-parent constructor calls via super(...), and static initializers (<clinit>)
  • Extract line numbers from the <span id="L123"> elements
  1. XML Analysis (getCoveredLinesForMethod()):

    • Parse the JaCoCo XML report to find covered line numbers for the specific caller method
    • Use method descriptors (JVM signatures) for precise matching, handling overloaded methods
    • Determine method boundaries to isolate lines belonging to the specific method
  2. Intersection Check (isPreciseMethodCovered()):

    • Find the intersection between target call lines (from HTML) and covered lines in the method (from XML)
    • If any intersection exists, the specific call site is covered

Corner cases handled (pragmatic heuristics):

  • Static initializers (<clinit>): treated specially since JaCoCo reporting patterns can differ; the filter falls back to a class-level HTML check for <clinit>.
  • Child constructor reaching parent constructor (<init>):
    • Detects when the caller class extends the target class (from HTML source).
    • Treats an explicit super(...) call as the constructor call-site.
    • Treats an implicit super() as covered by looking at the constructor declaration / class declaration (with extends) line when JaCoCo marks it covered.
  • Overloads: when the same third-party method name appears with multiple parameter lists in a class, we force the precise HTML+XML strategy since HTML cannot distinguish overloads.

Notes:

  • HTML reports show code and coverage but don't provide method-level granularity
  • XML reports provide precise line-level coverage per method but don't show actual code
  • Method overloading requires matching full JVM descriptors (method name + parameter types)
  • Determining method boundaries in XML requires analyzing all method start lines in a class
  • Constructor calls look different in bytecode (<init>) than in source (new ClassName())
  • Static initializers (<clinit>) need special handling

4. Path Finding

Why: Finding execution paths from public entry points to third-party methods in large codebases can be computationally expensive, and there may be thousands of paths to a single target. We need to find meaningful paths efficiently.

Implementation (current): Path finding is anchored on the direct call site and uses a reverse call graph BFS.

Step 1: Identify the call site (direct caller)

From the call graph, we record pairs (directCaller → thirdPartyMethod) for third-party calls that remain after coverage filtering.

Step 2: Fast-path when the direct caller is already public

If directCaller is itself a public method (an entry point), we record the path as:

[directCaller, thirdPartyMethod]

Step 3: Backward BFS to the first reachable public methods

If directCaller is not public, we BFS backwards through the reverse call graph starting at directCaller:

  • Traverse only project methods (skip third-party methods while searching).
  • Stop expanding a branch as soon as a public method is reached (we record the first public method on that branch).
  • Reconstruct each recorded path by reversing the backward chain and appending thirdPartyMethod at the end.

This keeps the search efficient and produces practical entry-point-to-call-site paths. We still use BFS, but we no longer enforce an explicit “shortest direct path” selection strategy beyond the natural behavior of BFS and the early-stop-at-first-public rule.

Why reverse traversal?

  • There are typically far fewer third-party method calls than public methods
  • Starting from call sites and working backward is much more efficient
  • It naturally identifies only the paths that actually reach third-party code

Project-only traversal: During backward traversal we skip third-party methods, so discovered paths are comprised of project methods up to the final third-party target.

5. Source Code Extraction with Spoon

Why: Extracting actual Java source code (not bytecode or Jimple IR) with proper handling of generics, annotations, inner classes, and adding contextual markers to guide reachability scenario generation.

Implementation: Fika uses Spoon, a library for Java source code analysis and transformation:

Initialization

  • Creates a MavenLauncher that understands Maven project structure
  • Builds a complete AST model of the source code
  • Uses no-classpath mode (doesn't require all dependencies to be on classpath)
  • Preserves comments for better code readability
  • Caches the model to avoid re-parsing for subsequent extractions

Method Extraction

Fika handles different method types:

Regular Methods (extractRegularMethod):

  • Finds the method by name and matches parameter types precisely
  • Handles method overloading by comparing JVM signatures
  • Uses SpoonMethodFinder.findRegularMethod() for signature matching

Constructors (extractConstructor):

  • Identifies constructors (represented as <init> in bytecode)
  • Matches parameter types to handle constructor overloading
  • Extracts the complete constructor body

Static Initializers (extractStaticInitializer):

  • Handles <clinit> (static initialization blocks)
  • Finds all static blocks in the class
  • Returns concatenated source of all static initializers

Notes:

  • Bytecode method signatures use JVM descriptors (e.g., (Ljava/lang/String;I)V)
  • Source code uses different type representations
  • Inner classes use $ in bytecode but . in source
  • Generic types are erased in bytecode but present in source
  • Method overloading requires exact parameter type matching

Path Tracking Comments

A unique feature of the source extraction is adding inline comments to indicate the execution path:

For each method in a path, Fika:

  1. Identifies the specific call site that leads to the next method in the path
  2. Uses PathCallFinder to locate the exact invocation in the AST
  3. Adds an inline comment after that statement:
    // PATH: Test should invoke the next ClassName.methodName(...) [step in execution path]

This helps the LLM understand which specific call in the method should be exercised by the reachability scenario.

Handling edge cases:

  • If Spoon's comment API fails (due to AST modification restrictions), falls back to string manipulation
  • Handles both method calls and constructor invocations (new ClassName())

Class Members Extraction

For reachability scenario generation, Fika also extracts class context:

Constructors: Constructors are extracted to help the reachability scenario instantiate the class.

Factory methods (when constructors are private): If the class has private constructors, Fika also extracts public static factory methods that return an instance of the class. These are included alongside constructors to give the reachability scenario generator a viable instantiation strategy.

Field Declarations: All instance and static field declarations are extracted to provide context about the class's state that may need to be initialized or validated in reachability scenarios.

Field-Modifying Methods: Methods that modify the class's fields, including:

  • Methods with direct field assignments (this.field = value or field = value)
  • Methods that call mutating operations on fields (e.g., list.add(...), map.put(...))
  • This captures any method that sets state, regardless of naming convention (e.g., "set...", "add...", "update...")
  • Has void as return type.

Imports: Fika scans all methods in the path and extracts:

  • All import statements from classes involved
  • Import statements for field types (excluding core Java packages and same-package types)
  • Filters to non-Java standard library imports
  • Removes duplicates and sorts alphabetically

6. Condition Count Calculation

Why: Not all paths are equally easy to test. Paths with many conditional branches (if statements, loops, switches) require more complex test inputs and edge case handling. But this step is not that important for Fika.

Implementation: The RecordCounter analyzes code complexity by counting control flow conditions:

For each method in the path, Fika counts:

  • If statements (CtIf): Conditional branches
  • For loops (CtFor): Traditional for loops
  • For-each loops (CtForEach): Enhanced for loops
  • While loops (CtWhile): Conditional loops
  • Do-while loops (CtDo): Post-condition loops
  • Switch statements (CtSwitch): Multi-way branches
  • Ternary operators (CtConditional): Inline conditionals (condition ? true : false)

Fika uses Spoon's AST to find all control flow elements in each method's body:

Caching: Condition counts are cached per method signature to avoid re-parsing.

The condition count is used to sort paths by complexity. When multiple paths reach the same third-party method, Fika prioritizes simpler paths (fewer conditions) because:

  • They're easier for LLMs to generate reachability scenarios for
  • Tests are more maintainable and readable
  • Less likely to require complex mocking or setup However, currently Fika generates reachability scenarios for all identified paths, not just the simplest ones.

7. Test Template Generation

The reachability scenario template generation is intentionally kept simple. Its only purpose is to provide:

  • Package name: Where the reachability scenario class should be located
  • Test class name: Generated from the path (e.g., EntryClass_CallerMethod_TargetClass_TargetMethodFikaTest)
  • Test method name: Derived from the entry point method name

The template (Template.java) contains basic JUnit boilerplate with placeholders:

These placeholders are replaced by actual values. The template is not critical for reachability scenario generation - modern LLMs can easily generate reachability scenario structure. It's mainly useful for maintaining consistent naming conventions.

Output Format

The API-Finder generates a comprehensive JSON report (third_party_apis_full_methods.json):

{
  "fullMethodsPaths": [
    {
      "entryPoint": "com.example.MyClass.publicMethod",
      "thirdPartyMethod": "org.library.ThirdParty.targetMethod",
      "directCaller": "com.example.MyClass.helperMethod",
      "path": [
        "com.example.MyClass.publicMethod",
        "com.example.MyClass.helperMethod",
        "org.library.ThirdParty.targetMethod"
      ],
      "methodSources": [
        "public void publicMethod() {\n    helperMethod(); // PATH: Test should invoke...\n}",
        "private void helperMethod() {\n    thirdParty.targetMethod(); // PATH: Test should invoke...\n}"
      ],
      "constructors": ["public MyClass() { ... }"],
      "fieldDeclarations": ["private String field;", "private List<Item> items;"],
      "setters": ["public void setField(String value) { ... }", "public void addItem(Item item) { ... }"],
      "imports": ["import org.library.ThirdParty;"],
      "testTemplate": "package com.example;\n\npublic class MyClass_helperMethod_ThirdParty_targetMethodFikaTest {\n    @Test\n    public void testPublicMethod() {\n        // TODO\n    }\n}",
      "conditionCount": 3,
      "callCount": 1,
      "covered": false
    }
  ]
}

Notes:

  • Method signatures in the real output include parameter types to distinguish overloads.
  • directCaller is the project method immediately before thirdPartyMethod in path.
  • methodSources contains project methods only (the third-party method body is intentionally omitted).

Paths are sorted by:

  1. Primary sort: Path length (ascending) - shorter paths first
  2. Secondary sort: Condition count (ascending) - simpler paths first

This prioritization ensures that reachability scenario generation focuses on the most tractable cases first.

Key Design Decisions

1. Public Methods Only as Entry Points

Decision: Only public methods are used as reachability scenario entry points.

Rationale:

  • Main goal of Fika is reachability analysis and public methods are indicators of intended usage
  • Aligns with testing best practices

2. Call-site Anchored Reverse BFS

Decision: Path finding starts from each third-party call site’s direct caller and uses reverse BFS to find reachable public entry points.

Rationale:

  • There are usually far fewer third-party call sites than public methods, so working backward is efficient.
  • Using the direct caller preserves call-site context (needed for coverage filtering and reachability scenario generation).
  • BFS plus “stop at first public method per branch” yields practical paths without attempting to enumerate all possible paths.

3. Reverse Call Graph Traversal

Decision: Path finding works backward from targets to entry points, then reconstructs paths forward.

Rationale:

  • Massively more efficient than forward search from thousands of entry points
  • Naturally prunes paths that don't reach any third-party code
  • Enables quick identification of all relevant entry points

4. Two-Phase Coverage Checking

Decision: Simple HTML check for single call sites, precise HTML+XML check for multiple call sites.

Rationale:

  • Performance: Parsing XML is expensive; avoid it when not needed
  • Accuracy: When necessary, combine HTML (for code context) with XML (for precise coverage)
  • Caching: Both approaches benefit from multi-level caching

5. Source Code Over Bytecode

Decision: Extract actual Java source code using Spoon rather than Jimple IR from Soot.

Rationale:

  • LLMs are trained on source code, not IR
  • Source preserves variable names, comments, and idioms
  • Generics and annotations are lost in bytecode
  • More readable for human verification

Stack

  • SootUp: Call graph construction and bytecode analysis
  • Spoon: Source code parsing, analysis, and transformation
  • JaCoCo Reports: Coverage information (HTML and XML)
  • Jsoup: HTML parsing for JaCoCo reports
  • Jackson/Gson: JSON serialization
  • SLF4J: Logging
  • PicoCLI: Command-line interface

Usage

See the main README for usage instructions.

Performance Considerations

  • Model Caching: Spoon models are cached and reused for all methods in a project
  • Multi-level Coverage Caching: Coverage decisions, HTML line numbers, and XML coverage data are all cached
  • Condition Caching: Method condition counts are cached to avoid re-parsing
  • Lazy Parsing: XML reports are only parsed when precise coverage checks are needed

Limitations

Below are known edge cases / minor bugs in api-finder:

  • Limited to static analysis - Cannot analyze dynamically loaded classes, cannot handle reflection-based method calls or lambda expressions.
  • We consider public methods inside private inner classes as public entry points.
  • Spoon cannot always find sources (e.g., some anonymous/complex constructor patterns), which can lead to paths being skipped due to missing source extraction.
  • Coverage can be inaccurate for nested static classes that extend a parent class.
  • If the same third-party method appears multiple times in the same caller method, the distinction is not made. All those instances are considered as one call site.
  • Coverage checking and source lookup do not recurse into superclasses. That means, if an implementation lives only in a parent class and not in the class under consideration itself, it may not be retrieved.
  • Calls to the iterator method are intentionally ignored.
  • For inner classes within an outer class that has a constructor that implicitly/explicitly calls super(...), if the outer constructor line is covered, we may treat the inner class constructor’s <init>-related call site as covered as well.

Future Improvements

  • Integration with other coverage tools beyond JaCoCo
  • Incremental analysis (only analyze changed code)