Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.itsaky.androidide.lsp.java.refactor

import com.itsaky.androidide.lsp.refactor.TextSpan
import jdkx.lang.model.element.Modifier
import openjdk.source.tree.BlockTree
import openjdk.source.tree.ClassTree
import openjdk.source.tree.CompilationUnitTree
import openjdk.source.tree.MethodTree
import openjdk.source.tree.Tree
import openjdk.source.tree.VariableTree
import openjdk.source.util.SourcePositions
import openjdk.source.util.TreePath

/**
* The class member the new method becomes a sibling of (R4).
*
* "Direct member of a `ClassTree`" covers every anchor uniformly: a method, a constructor, an
* initializer block, and a field whose initializer holds a lambda. Java has no local-method form, so
* unlike Kotlin there is no insert-before case and no "nowhere to anchor" refusal.
*/
internal class AnchorMember(
val path: TreePath,
val classPath: TreePath,
val span: TextSpan,
val isStatic: Boolean,
val method: MethodTree?,
)

/**
* The nearest ancestor that is a direct member of a class.
*
* A nested class is never climbed past: a region inside a member of an inner, local or anonymous class
* anchors on that member, so the new method lands in the class whose members the region reads.
*/
internal fun anchorMemberFor(
regionPath: TreePath,
root: CompilationUnitTree,
positions: SourcePositions,
): AnchorMember? {
var current: TreePath = regionPath
while (true) {
val parent = current.parentPath ?: return null
if (parent.leaf is ClassTree) {
val member = current.leaf
val span = spanOf(root, positions, member) ?: return null
return AnchorMember(
path = current,
classPath = parent,
span = span,
isStatic = isStaticMember(member),
method = member as? MethodTree,
)
}
current = parent
}
}

/** A static anchor forces a `static` method: no instance to resolve `this` or an instance member on. */
private fun isStaticMember(member: Tree): Boolean =
when (member) {
is MethodTree -> Modifier.STATIC in member.modifiers.flags
is VariableTree -> Modifier.STATIC in member.modifiers.flags
is BlockTree -> member.isStatic
else -> false
}

/** The trees the region actually covers: one expression, or each statement of the range. */
internal fun regionPathsOf(region: ExtractionRegion): List<TreePath> =
when (region) {
is ExtractionRegion.Expression -> {
listOf(region.path)
}

is ExtractionRegion.Statements -> {
val blockPath = region.path.parentPath
if (blockPath == null) listOf(region.path) else region.statements.map { TreePath(blockPath, it) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package com.itsaky.androidide.lsp.java.refactor

import com.itsaky.androidide.lsp.refactor.TextSpan
import jdkx.lang.model.element.Element
import jdkx.lang.model.element.VariableElement
import openjdk.source.tree.AssignmentTree
import openjdk.source.tree.CompilationUnitTree
import openjdk.source.tree.CompoundAssignmentTree
import openjdk.source.tree.IdentifierTree
import openjdk.source.tree.Tree
import openjdk.source.tree.UnaryTree
import openjdk.source.util.SourcePositions
import openjdk.source.util.TreePath
import openjdk.source.util.TreePathScanner
import openjdk.source.util.Trees

internal class Reference(
val element: Element,
val offset: Int,
)

/**
* Every named reference the region makes, in source order.
*
* Identifiers only: a member select's own selector resolves to a field or method, which needs nothing,
* and its base is an identifier this already sees. Nested lambdas and local classes **are** descended
* into, since a local they capture is a local the new method must be handed.
*/
internal fun collectReferences(
regionPaths: List<TreePath>,
root: CompilationUnitTree,
positions: SourcePositions,
trees: Trees,
): List<Reference> {
val references = mutableListOf<Reference>()

fun consider(path: TreePath) {
val leaf = path.leaf
if (leaf !is IdentifierTree) return
val element = runCatching { trees.getElement(path) }.getOrNull() ?: return
val span = spanOf(root, positions, leaf) ?: return
references += Reference(element, span.start)
}

val scanner =
object : TreePathScanner<Unit, Unit>() {
override fun scan(
tree: Tree?,
p: Unit?,
): Unit? {
if (tree == null) return null
consider(TreePath(currentPath, tree))
return super.scan(tree, p)
}
}

regionPaths.forEach { path ->
consider(path)
scanner.scan(path, null)
}
return references.sortedBy { it.offset }
}

/**
* The name of the variable the region reassigns but does not declare, or null when there is none (R7).
*
* Only a reassignment of the variable *itself* counts. An element write through a captured reference
* (`arr[i] = x`) mutates what the caller can already see, so it needs no rule -- the same distinction
* `writeOffsetsFor` draws for extract variable.
*/
internal fun outerReassignmentIn(
regionPaths: List<TreePath>,
span: TextSpan,
anchor: AnchorMember,
root: CompilationUnitTree,
trees: Trees,
positions: SourcePositions,
): String? {
var found: String? = null

fun consider(path: TreePath) {
if (found != null) return
val target =
when (val leaf = path.leaf) {
is AssignmentTree -> leaf.variable
is CompoundAssignmentTree -> leaf.variable
is UnaryTree -> if (leaf.kind in INCREMENT_KINDS) leaf.expression else null
else -> null
} as? IdentifierTree ?: return

val element = runCatching { trees.getElement(TreePath(path, target)) }.getOrNull() ?: return
if (element.kind !in LOCAL_KINDS) return
val declaration = declarationSpanOf(element, root, trees, positions) ?: return
if (span.contains(declaration)) return
if (!anchor.span.contains(declaration)) return
found = element.simpleName.toString()
}

val scanner =
object : TreePathScanner<Unit, Unit>() {
override fun scan(
tree: Tree?,
p: Unit?,
): Unit? {
if (tree == null) return null
consider(TreePath(currentPath, tree))
return super.scan(tree, p)
}
}

regionPaths.forEach { path ->
consider(path)
scanner.scan(path, null)
}
return found
}

/**
* The variables that become parameters: referenced, declared inside the anchor member, declared outside
* the region. In first textual appearance order, so the signature reads in the order the body uses it.
*
* A field needs nothing -- the new method is a member of the same class -- and a declaration in another
* file cannot be a local at all.
*/
internal fun capturedVariablesIn(
references: List<Reference>,
span: TextSpan,
anchor: AnchorMember,
root: CompilationUnitTree,
trees: Trees,
positions: SourcePositions,
): List<VariableElement> {
val captured = LinkedHashMap<VariableElement, Unit>()
for (reference in references) {
val element = reference.element as? VariableElement ?: continue
if (element.kind !in LOCAL_KINDS) continue
val declaration = declarationSpanOf(element, root, trees, positions) ?: continue
if (span.contains(declaration)) continue
if (!anchor.span.contains(declaration)) continue
captured[element] = Unit
}
return captured.keys.toList()
}

internal fun declarationSpanOf(
element: Element,
root: CompilationUnitTree,
trees: Trees,
positions: SourcePositions,
): TextSpan? {
val path = runCatching { trees.getPath(element) }.getOrNull() ?: return null
if (path.compilationUnit !== root) return null
return spanOf(root, positions, path.leaf)
}

/** Whether [other] lies entirely inside this span. */
internal fun TextSpan.contains(other: TextSpan): Boolean = start <= other.start && other.end <= end
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package com.itsaky.androidide.lsp.java.refactor

import com.itsaky.androidide.lsp.refactor.TextSpan
import jdkx.lang.model.element.Element
import jdkx.lang.model.element.NestingKind
import jdkx.lang.model.element.TypeElement
import jdkx.lang.model.element.TypeParameterElement
import jdkx.lang.model.type.ArrayType
import jdkx.lang.model.type.DeclaredType
import jdkx.lang.model.type.TypeMirror
import jdkx.lang.model.type.TypeVariable
import jdkx.lang.model.type.UnionType
import jdkx.lang.model.type.WildcardType
import openjdk.source.tree.CompilationUnitTree
import openjdk.source.util.SourcePositions
import openjdk.source.util.Trees

/** Whether [element] is a type parameter declared on the anchor method itself (R10). */
internal fun isAnchorTypeParameter(
element: Element,
anchorMethodElement: Element?,
): Boolean =
element is TypeParameterElement &&
anchorMethodElement != null &&
runCatching { element.genericElement == anchorMethodElement }.getOrDefault(false)

/**
* Whether [element] is a local or anonymous class the region uses but does not contain. Its name is
* reachable only from inside the anchor member, so it stops resolving once the body moves.
*/
internal fun isCapturedLocalType(
element: Element,
span: TextSpan,
root: CompilationUnitTree,
trees: Trees,
positions: SourcePositions,
): Boolean {
// A reference to the class *itself* is only one of the shapes: `new Helper()` resolves to Helper's
// constructor and `Helper.CONST` to a field, so a member's own declaring class is asked about too.
val type =
element as? TypeElement
?: element.enclosingElement as? TypeElement
?: return false
if (type.nestingKind != NestingKind.LOCAL && type.nestingKind != NestingKind.ANONYMOUS) return false
val declaration = declarationSpanOf(type, root, trees, positions) ?: return false
return !span.contains(declaration)
}

/** The local class a reference names, whether directly or through one of its members. */
internal fun localTypeNameOf(element: Element): String {
val type = element as? TypeElement ?: element.enclosingElement as? TypeElement ?: return element.simpleName.toString()
return type.simpleName.toString().ifEmpty { element.simpleName.toString() }
}

/**
* The name of a local or anonymous class appearing anywhere in [type], or null when there is none.
*
* A depth limit rather than a visited set: `Enum<E extends Enum<E>>` is a real shape, and comparing
* `TypeMirror`s for identity is not reliable enough to terminate on.
*/
internal fun localTypeNameIn(
type: TypeMirror,
depth: Int = 0,
): String? {
if (depth > MAX_TYPE_DEPTH) return null
return when (type) {
is DeclaredType -> {
val element = runCatching { type.asElement() }.getOrNull() as? TypeElement
if (element != null &&
(element.nestingKind == NestingKind.LOCAL || element.nestingKind == NestingKind.ANONYMOUS)
) {
element.simpleName.toString().ifEmpty { "anonymous class" }
} else {
type.typeArguments.firstNotNullOfOrNull { localTypeNameIn(it, depth + 1) }
}
}

is ArrayType -> {
localTypeNameIn(type.componentType, depth + 1)
}

is WildcardType -> {
type.extendsBound?.let { localTypeNameIn(it, depth + 1) }
?: type.superBound?.let { localTypeNameIn(it, depth + 1) }
}

is UnionType -> {
type.alternatives.firstNotNullOfOrNull { localTypeNameIn(it, depth + 1) }
}

else -> {
null
}
}
}

/** The name of a type variable declared on the anchor method appearing anywhere in [type] (R10). */
internal fun anchorTypeVariableIn(
type: TypeMirror,
anchorMethodElement: Element?,
depth: Int = 0,
): String? {
if (anchorMethodElement == null || depth > MAX_TYPE_DEPTH) return null
return when (type) {
is TypeVariable -> {
val element = runCatching { type.asElement() }.getOrNull()
if (isAnchorTypeParameter(element ?: return null, anchorMethodElement)) {
element.simpleName.toString()
} else {
null
}
}

is DeclaredType -> {
type.typeArguments.firstNotNullOfOrNull { anchorTypeVariableIn(it, anchorMethodElement, depth + 1) }
}

is ArrayType -> {
anchorTypeVariableIn(type.componentType, anchorMethodElement, depth + 1)
}

is WildcardType -> {
type.extendsBound?.let { anchorTypeVariableIn(it, anchorMethodElement, depth + 1) }
?: type.superBound?.let { anchorTypeVariableIn(it, anchorMethodElement, depth + 1) }
}

is UnionType -> {
type.alternatives.firstNotNullOfOrNull { anchorTypeVariableIn(it, anchorMethodElement, depth + 1) }
}

else -> {
null
}
}
}

private const val MAX_TYPE_DEPTH = 8

/**
* Renders a type as source, shortened only where the file already resolves the short form.
*
* The import sets are read once per plan rather than per type: every candidate in one plan renders
* against the same file.
*/
internal class TypeNames(
root: CompilationUnitTree,
) {
private val imported = importedNamesOf(root)
private val starred = starImportedPackagesOf(root)

fun render(type: TypeMirror): String? {
val text = runCatching { type.toString() }.getOrNull() ?: return null
if (isUnrenderableTypeText(text)) return null
if (isValuelessKind(type.kind)) return null
return shortenTypeText(text, imported, starred)
}
}
Loading