Skip to content

Commit 86b5e1e

Browse files
committed
ADFA-5048: Add the region analysis primitives
The three concerns extract method's analysis needs before it can derive a signature, each independent of the others and of the plan model: - AnchorMember.kt - the class member the new method becomes a sibling of. "Nearest ancestor that is a direct member of a ClassTree" covers method, constructor, initializer and field uniformly, and decides `static`. - RegionReferences.kt - what the region names: every identifier in source order, which of those are locals declared inside the anchor but outside the region (the future parameters), and whether the region reassigns one it does not declare. - RegionTypes.kt - rendering a type as source, shortened only where the file already resolves the short form, plus the two shapes that cannot be written out: a local or anonymous class, and a type variable owned by the anchor. Split into three files rather than one because they only meet in the entry point that follows, and 1000 lines in one file is not reviewable. No caller yet; the analysis that uses them lands next.
1 parent b095c4f commit 86b5e1e

3 files changed

Lines changed: 392 additions & 0 deletions

File tree

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package com.itsaky.androidide.lsp.java.refactor
2+
3+
import com.itsaky.androidide.lsp.refactor.TextSpan
4+
import jdkx.lang.model.element.Modifier
5+
import openjdk.source.tree.BlockTree
6+
import openjdk.source.tree.ClassTree
7+
import openjdk.source.tree.CompilationUnitTree
8+
import openjdk.source.tree.MethodTree
9+
import openjdk.source.tree.Tree
10+
import openjdk.source.tree.VariableTree
11+
import openjdk.source.util.SourcePositions
12+
import openjdk.source.util.TreePath
13+
14+
/**
15+
* The class member the new method becomes a sibling of (R4).
16+
*
17+
* "Direct member of a `ClassTree`" covers every anchor uniformly: a method, a constructor, an
18+
* initializer block, and a field whose initializer holds a lambda. Java has no local-method form, so
19+
* unlike Kotlin there is no insert-before case and no "nowhere to anchor" refusal.
20+
*/
21+
internal class AnchorMember(
22+
val path: TreePath,
23+
val classPath: TreePath,
24+
val span: TextSpan,
25+
val isStatic: Boolean,
26+
val method: MethodTree?,
27+
)
28+
29+
/**
30+
* The nearest ancestor that is a direct member of a class.
31+
*
32+
* A nested class is never climbed past: a region inside a member of an inner, local or anonymous class
33+
* anchors on that member, so the new method lands in the class whose members the region reads.
34+
*/
35+
internal fun anchorMemberFor(
36+
regionPath: TreePath,
37+
root: CompilationUnitTree,
38+
positions: SourcePositions,
39+
): AnchorMember? {
40+
var current: TreePath = regionPath
41+
while (true) {
42+
val parent = current.parentPath ?: return null
43+
if (parent.leaf is ClassTree) {
44+
val member = current.leaf
45+
val span = spanOf(root, positions, member) ?: return null
46+
return AnchorMember(
47+
path = current,
48+
classPath = parent,
49+
span = span,
50+
isStatic = isStaticMember(member),
51+
method = member as? MethodTree,
52+
)
53+
}
54+
current = parent
55+
}
56+
}
57+
58+
/** A static anchor forces a `static` method: no instance to resolve `this` or an instance member on. */
59+
private fun isStaticMember(member: Tree): Boolean =
60+
when (member) {
61+
is MethodTree -> Modifier.STATIC in member.modifiers.flags
62+
is VariableTree -> Modifier.STATIC in member.modifiers.flags
63+
is BlockTree -> member.isStatic
64+
else -> false
65+
}
66+
67+
/** The trees the region actually covers: one expression, or each statement of the range. */
68+
internal fun regionPathsOf(region: ExtractionRegion): List<TreePath> =
69+
when (region) {
70+
is ExtractionRegion.Expression -> {
71+
listOf(region.path)
72+
}
73+
74+
is ExtractionRegion.Statements -> {
75+
val blockPath = region.path.parentPath
76+
if (blockPath == null) listOf(region.path) else region.statements.map { TreePath(blockPath, it) }
77+
}
78+
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package com.itsaky.androidide.lsp.java.refactor
2+
3+
import com.itsaky.androidide.lsp.refactor.TextSpan
4+
import jdkx.lang.model.element.Element
5+
import jdkx.lang.model.element.VariableElement
6+
import openjdk.source.tree.AssignmentTree
7+
import openjdk.source.tree.CompilationUnitTree
8+
import openjdk.source.tree.CompoundAssignmentTree
9+
import openjdk.source.tree.IdentifierTree
10+
import openjdk.source.tree.Tree
11+
import openjdk.source.tree.UnaryTree
12+
import openjdk.source.util.SourcePositions
13+
import openjdk.source.util.TreePath
14+
import openjdk.source.util.TreePathScanner
15+
import openjdk.source.util.Trees
16+
17+
internal class Reference(
18+
val element: Element,
19+
val offset: Int,
20+
)
21+
22+
/**
23+
* Every named reference the region makes, in source order.
24+
*
25+
* Identifiers only: a member select's own selector resolves to a field or method, which needs nothing,
26+
* and its base is an identifier this already sees. Nested lambdas and local classes **are** descended
27+
* into, since a local they capture is a local the new method must be handed.
28+
*/
29+
internal fun collectReferences(
30+
regionPaths: List<TreePath>,
31+
root: CompilationUnitTree,
32+
positions: SourcePositions,
33+
trees: Trees,
34+
): List<Reference> {
35+
val references = mutableListOf<Reference>()
36+
37+
fun consider(path: TreePath) {
38+
val leaf = path.leaf
39+
if (leaf !is IdentifierTree) return
40+
val element = runCatching { trees.getElement(path) }.getOrNull() ?: return
41+
val span = spanOf(root, positions, leaf) ?: return
42+
references += Reference(element, span.start)
43+
}
44+
45+
val scanner =
46+
object : TreePathScanner<Unit, Unit>() {
47+
override fun scan(
48+
tree: Tree?,
49+
p: Unit?,
50+
): Unit? {
51+
if (tree == null) return null
52+
consider(TreePath(currentPath, tree))
53+
return super.scan(tree, p)
54+
}
55+
}
56+
57+
regionPaths.forEach { path ->
58+
consider(path)
59+
scanner.scan(path, null)
60+
}
61+
return references.sortedBy { it.offset }
62+
}
63+
64+
/**
65+
* The name of the variable the region reassigns but does not declare, or null when there is none (R7).
66+
*
67+
* Only a reassignment of the variable *itself* counts. An element write through a captured reference
68+
* (`arr[i] = x`) mutates what the caller can already see, so it needs no rule -- the same distinction
69+
* `writeOffsetsFor` draws for extract variable.
70+
*/
71+
internal fun outerReassignmentIn(
72+
regionPaths: List<TreePath>,
73+
span: TextSpan,
74+
anchor: AnchorMember,
75+
root: CompilationUnitTree,
76+
trees: Trees,
77+
positions: SourcePositions,
78+
): String? {
79+
var found: String? = null
80+
81+
fun consider(path: TreePath) {
82+
if (found != null) return
83+
val target =
84+
when (val leaf = path.leaf) {
85+
is AssignmentTree -> leaf.variable
86+
is CompoundAssignmentTree -> leaf.variable
87+
is UnaryTree -> if (leaf.kind in INCREMENT_KINDS) leaf.expression else null
88+
else -> null
89+
} as? IdentifierTree ?: return
90+
91+
val element = runCatching { trees.getElement(TreePath(path, target)) }.getOrNull() ?: return
92+
if (element.kind !in LOCAL_KINDS) return
93+
val declaration = declarationSpanOf(element, root, trees, positions) ?: return
94+
if (span.contains(declaration)) return
95+
if (!anchor.span.contains(declaration)) return
96+
found = element.simpleName.toString()
97+
}
98+
99+
val scanner =
100+
object : TreePathScanner<Unit, Unit>() {
101+
override fun scan(
102+
tree: Tree?,
103+
p: Unit?,
104+
): Unit? {
105+
if (tree == null) return null
106+
consider(TreePath(currentPath, tree))
107+
return super.scan(tree, p)
108+
}
109+
}
110+
111+
regionPaths.forEach { path ->
112+
consider(path)
113+
scanner.scan(path, null)
114+
}
115+
return found
116+
}
117+
118+
/**
119+
* The variables that become parameters: referenced, declared inside the anchor member, declared outside
120+
* the region. In first textual appearance order, so the signature reads in the order the body uses it.
121+
*
122+
* A field needs nothing -- the new method is a member of the same class -- and a declaration in another
123+
* file cannot be a local at all.
124+
*/
125+
internal fun capturedVariablesIn(
126+
references: List<Reference>,
127+
span: TextSpan,
128+
anchor: AnchorMember,
129+
root: CompilationUnitTree,
130+
trees: Trees,
131+
positions: SourcePositions,
132+
): List<VariableElement> {
133+
val captured = LinkedHashMap<VariableElement, Unit>()
134+
for (reference in references) {
135+
val element = reference.element as? VariableElement ?: continue
136+
if (element.kind !in LOCAL_KINDS) continue
137+
val declaration = declarationSpanOf(element, root, trees, positions) ?: continue
138+
if (span.contains(declaration)) continue
139+
if (!anchor.span.contains(declaration)) continue
140+
captured[element] = Unit
141+
}
142+
return captured.keys.toList()
143+
}
144+
145+
internal fun declarationSpanOf(
146+
element: Element,
147+
root: CompilationUnitTree,
148+
trees: Trees,
149+
positions: SourcePositions,
150+
): TextSpan? {
151+
val path = runCatching { trees.getPath(element) }.getOrNull() ?: return null
152+
if (path.compilationUnit !== root) return null
153+
return spanOf(root, positions, path.leaf)
154+
}
155+
156+
/** Whether [other] lies entirely inside this span. */
157+
internal fun TextSpan.contains(other: TextSpan): Boolean = start <= other.start && other.end <= end
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package com.itsaky.androidide.lsp.java.refactor
2+
3+
import com.itsaky.androidide.lsp.refactor.TextSpan
4+
import jdkx.lang.model.element.Element
5+
import jdkx.lang.model.element.NestingKind
6+
import jdkx.lang.model.element.TypeElement
7+
import jdkx.lang.model.element.TypeParameterElement
8+
import jdkx.lang.model.type.ArrayType
9+
import jdkx.lang.model.type.DeclaredType
10+
import jdkx.lang.model.type.TypeMirror
11+
import jdkx.lang.model.type.TypeVariable
12+
import jdkx.lang.model.type.UnionType
13+
import jdkx.lang.model.type.WildcardType
14+
import openjdk.source.tree.CompilationUnitTree
15+
import openjdk.source.util.SourcePositions
16+
import openjdk.source.util.Trees
17+
18+
/** Whether [element] is a type parameter declared on the anchor method itself (R10). */
19+
internal fun isAnchorTypeParameter(
20+
element: Element,
21+
anchorMethodElement: Element?,
22+
): Boolean =
23+
element is TypeParameterElement &&
24+
anchorMethodElement != null &&
25+
runCatching { element.genericElement == anchorMethodElement }.getOrDefault(false)
26+
27+
/**
28+
* Whether [element] is a local or anonymous class the region uses but does not contain. Its name is
29+
* reachable only from inside the anchor member, so it stops resolving once the body moves.
30+
*/
31+
internal fun isCapturedLocalType(
32+
element: Element,
33+
span: TextSpan,
34+
root: CompilationUnitTree,
35+
trees: Trees,
36+
positions: SourcePositions,
37+
): Boolean {
38+
// A reference to the class *itself* is only one of the shapes: `new Helper()` resolves to Helper's
39+
// constructor and `Helper.CONST` to a field, so a member's own declaring class is asked about too.
40+
val type =
41+
element as? TypeElement
42+
?: element.enclosingElement as? TypeElement
43+
?: return false
44+
if (type.nestingKind != NestingKind.LOCAL && type.nestingKind != NestingKind.ANONYMOUS) return false
45+
val declaration = declarationSpanOf(type, root, trees, positions) ?: return false
46+
return !span.contains(declaration)
47+
}
48+
49+
/** The local class a reference names, whether directly or through one of its members. */
50+
internal fun localTypeNameOf(element: Element): String {
51+
val type = element as? TypeElement ?: element.enclosingElement as? TypeElement ?: return element.simpleName.toString()
52+
return type.simpleName.toString().ifEmpty { element.simpleName.toString() }
53+
}
54+
55+
/**
56+
* The name of a local or anonymous class appearing anywhere in [type], or null when there is none.
57+
*
58+
* A depth limit rather than a visited set: `Enum<E extends Enum<E>>` is a real shape, and comparing
59+
* `TypeMirror`s for identity is not reliable enough to terminate on.
60+
*/
61+
internal fun localTypeNameIn(
62+
type: TypeMirror,
63+
depth: Int = 0,
64+
): String? {
65+
if (depth > MAX_TYPE_DEPTH) return null
66+
return when (type) {
67+
is DeclaredType -> {
68+
val element = runCatching { type.asElement() }.getOrNull() as? TypeElement
69+
if (element != null &&
70+
(element.nestingKind == NestingKind.LOCAL || element.nestingKind == NestingKind.ANONYMOUS)
71+
) {
72+
element.simpleName.toString().ifEmpty { "anonymous class" }
73+
} else {
74+
type.typeArguments.firstNotNullOfOrNull { localTypeNameIn(it, depth + 1) }
75+
}
76+
}
77+
78+
is ArrayType -> {
79+
localTypeNameIn(type.componentType, depth + 1)
80+
}
81+
82+
is WildcardType -> {
83+
type.extendsBound?.let { localTypeNameIn(it, depth + 1) }
84+
?: type.superBound?.let { localTypeNameIn(it, depth + 1) }
85+
}
86+
87+
is UnionType -> {
88+
type.alternatives.firstNotNullOfOrNull { localTypeNameIn(it, depth + 1) }
89+
}
90+
91+
else -> {
92+
null
93+
}
94+
}
95+
}
96+
97+
/** The name of a type variable declared on the anchor method appearing anywhere in [type] (R10). */
98+
internal fun anchorTypeVariableIn(
99+
type: TypeMirror,
100+
anchorMethodElement: Element?,
101+
depth: Int = 0,
102+
): String? {
103+
if (anchorMethodElement == null || depth > MAX_TYPE_DEPTH) return null
104+
return when (type) {
105+
is TypeVariable -> {
106+
val element = runCatching { type.asElement() }.getOrNull()
107+
if (isAnchorTypeParameter(element ?: return null, anchorMethodElement)) {
108+
element.simpleName.toString()
109+
} else {
110+
null
111+
}
112+
}
113+
114+
is DeclaredType -> {
115+
type.typeArguments.firstNotNullOfOrNull { anchorTypeVariableIn(it, anchorMethodElement, depth + 1) }
116+
}
117+
118+
is ArrayType -> {
119+
anchorTypeVariableIn(type.componentType, anchorMethodElement, depth + 1)
120+
}
121+
122+
is WildcardType -> {
123+
type.extendsBound?.let { anchorTypeVariableIn(it, anchorMethodElement, depth + 1) }
124+
?: type.superBound?.let { anchorTypeVariableIn(it, anchorMethodElement, depth + 1) }
125+
}
126+
127+
is UnionType -> {
128+
type.alternatives.firstNotNullOfOrNull { anchorTypeVariableIn(it, anchorMethodElement, depth + 1) }
129+
}
130+
131+
else -> {
132+
null
133+
}
134+
}
135+
}
136+
137+
private const val MAX_TYPE_DEPTH = 8
138+
139+
/**
140+
* Renders a type as source, shortened only where the file already resolves the short form.
141+
*
142+
* The import sets are read once per plan rather than per type: every candidate in one plan renders
143+
* against the same file.
144+
*/
145+
internal class TypeNames(
146+
root: CompilationUnitTree,
147+
) {
148+
private val imported = importedNamesOf(root)
149+
private val starred = starImportedPackagesOf(root)
150+
151+
fun render(type: TypeMirror): String? {
152+
val text = runCatching { type.toString() }.getOrNull() ?: return null
153+
if (isUnrenderableTypeText(text)) return null
154+
if (isValuelessKind(type.kind)) return null
155+
return shortenTypeText(text, imported, starred)
156+
}
157+
}

0 commit comments

Comments
 (0)