diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000000..e77944f6d1 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "WebSearch" + ], + "deny": [], + "ask": [] + } +} \ No newline at end of file diff --git a/rewrite-kotlin2/Kotlin2.md b/rewrite-kotlin2/Kotlin2.md new file mode 100644 index 0000000000..96e875fceb --- /dev/null +++ b/rewrite-kotlin2/Kotlin2.md @@ -0,0 +1,296 @@ +# Kotlin 2 Language Support Implementation Plan + +This document tracks the implementation plan for adding Kotlin 2 language support to OpenRewrite. + +## K2 Compiler Architecture Changes Summary + +### Why KotlinTreeParserVisitor Needs Major Refactoring + +The Kotlin 2.0 release introduced the **K2 compiler** with a completely rewritten frontend that fundamentally changes how Kotlin code is parsed and analyzed. This necessitates significant refactoring of OpenRewrite's `KotlinTreeParserVisitor`. + +#### Key Architectural Shifts: + +1. **From PSI+BindingContext to FIR** + - **K1 (Old)**: Used PSI trees with a separate BindingContext (huge hash maps) to store semantic information + - **K2 (New)**: Uses FIR (Frontend Intermediate Representation) with semantic information embedded directly in the tree structure + - **Impact**: KotlinTreeParserVisitor can no longer rely on BindingContext lookups; must interact with FIR APIs + +2. **New Compilation Pipeline** + - **K1**: PSI → Resolution → BindingContext → Backend + - **K2**: PSI → RawFIR → Multiple FIR Resolution Phases → Resolved FIR → Backend + - **Impact**: Type resolution and semantic analysis happen through FIR transformations, not descriptor-based APIs + +3. **Performance Improvements** + - Up to **94% faster compilation** (e.g., Anki-Android: 57.7s → 29.7s) + - Achieved through unified data structure, better cache utilization, elimination of redundant lookups + - Tree traversal is more efficient than hash map lookups in BindingContext + +4. **API Changes** + - Old descriptor-based APIs deprecated + - New FIR extension points: `FirDeclarationGenerationExtension`, `FirStatusTransformerExtension`, etc. + - Compiler plugins must be rewritten using new FIR Plugin API + +5. **What This Means for OpenRewrite** + - Existing `KotlinTreeParserVisitor` extends `KtVisitor` and uses K1 APIs + - Must create new visitor that works with FIR representations + - Need to map FIR elements to OpenRewrite's LST model instead of PSI+BindingContext + - Type attribution must come from FIR's resolved phases, not BindingContext + +## Overview + +This plan outlines the steps needed to implement a Kotlin 2 parser and AST model that integrates with OpenRewrite's Lossless Semantic Tree (LST) framework, accounting for the fundamental architectural changes in the K2 compiler. + +### Architecture Approach + +As a JVM-based language, Kotlin 2's LST implementation will: +- Define a `K2` interface that extends from `J` (Java's LST interface) +- Reuse common JVM constructs from the J model (classes, methods, fields, etc.) +- Leverage the existing `K` interface patterns for consistency +- Add Kotlin 2-specific constructs to the K2 interface (context receivers, value classes, etc.) +- Follow the established pattern used by existing Kotlin (`K extends J`), Groovy (`G extends J`), and Scala (`S extends J`) + +### Composition Pattern + +When implementing Kotlin 2-specific LST elements, we will use composition of J elements rather than duplication. This ensures that Java-focused recipes can still operate on Kotlin 2 code by accessing the composed J elements. For example: + +- Kotlin 2's context receivers might compose `J.MethodDeclaration` parameters +- Value classes could compose `J.ClassDeclaration` with special markers +- Sealed interfaces might compose `J.ClassDeclaration` with sealed modifiers + +This composition approach maximizes recipe reusability across all JVM languages. + +## Implementation Phases + +### Phase 1: Parser Implementation (First Priority) +- [ ] Create `Kotlin2Parser` class implementing `Parser` interface +- [ ] Integrate with Kotlin 2 compiler toolchain (K2 frontend) +- [ ] Implement builder pattern with classpath/dependency management +- [ ] Set up compiler configuration and error handling +- [ ] Support both Kotlin 1.x and 2.x language features + +The parser is the entry point and must be implemented first, following patterns from `KotlinParser`, `GroovyParser`, and `ScalaParser`. + +### Phase 2: Parser Visitor Implementation (Second Priority) +- [ ] Create `Kotlin2ParserVisitor` implementing Kotlin 2 compiler's AST visitor +- [ ] Map Kotlin 2 compiler AST elements to K2/J LST model +- [ ] Handle all Kotlin 2 language constructs +- [ ] Preserve formatting, comments, and type information +- [ ] Implement visitor methods for each Kotlin 2 AST node type +- [ ] Support backward compatibility with Kotlin 1.x features + +The Parser Visitor bridges the Kotlin 2 compiler's internal AST with OpenRewrite's LST model. + +### Phase 3: Visitor Infrastructure Skeleton +- [ ] Create `Kotlin2Visitor` extending `JavaVisitor` + - [ ] Override `isAcceptable()` and `getLanguage()` + - [ ] Add skeleton visit methods for future K2 elements +- [ ] Create `Kotlin2IsoVisitor` extending `JavaIsoVisitor` + - [ ] Override methods to provide type-safe transformations +- [ ] Create `Kotlin2Printer` extending `Kotlin2Visitor` + - [ ] Implement LST to source code conversion + - [ ] Create inner `Kotlin2JavaPrinter` for J elements +- [ ] Create supporting classes: `K2Space`, `K2LeftPadded`, `K2RightPadded`, `K2Container` + - [ ] Define Location enums for Kotlin 2-specific formatting + +This infrastructure must be in place before implementing LST elements. + +### Phase 4: Testing Infrastructure +- [ ] Create `Assertions.java` class with `kotlin2()` methods +- [ ] Implement parse-only overload for round-trip testing +- [ ] Implement before/after overload for transformation testing +- [ ] Configure Kotlin2Parser with appropriate classpath +- [ ] Create `org.openrewrite.kotlin2.tree` test package + +The Assertions class is the foundation for all Kotlin 2 LST testing. Each LST element gets a test in `org.openrewrite.kotlin2.tree` that uses `rewriteRun()` with `kotlin2()` assertions to verify parse → print → parse idempotency. + +### Phase 5: Core LST Infrastructure +- [ ] Create `rewrite-kotlin2` module structure +- [ ] Define `K2` interface extending `J` +- [ ] Implement Kotlin 2-specific AST classes in K2 +- [ ] Design composition strategy for Kotlin 2 constructs using J elements +- [ ] Write unit tests for each LST element in tree package + +### Phase 6: Advanced Kotlin 2 Features +- [ ] Add type attribution support from K2 compiler +- [ ] Handle Kotlin 2-specific features (context receivers, value classes, sealed interfaces, etc.) +- [ ] Implement formatting preservation for Kotlin 2 syntax +- [ ] Support migration paths from Kotlin 1.x to 2.x + +### Phase 7: Testing & Validation +- [ ] Create comprehensive test suite beyond tree tests +- [ ] Implement Kotlin 2 TCK (Technology Compatibility Kit) +- [ ] Validate LST round-trip accuracy +- [ ] Performance benchmarking against existing KotlinParser + +### Phase 8: Recipe Support +- [ ] Implement common Kotlin 2 refactoring recipes +- [ ] Create Kotlin 2-specific visitor utilities +- [ ] Document recipe development patterns +- [ ] Migration recipes from Kotlin 1.x to 2.x + +## Technical Considerations + +### Key Kotlin 2 Features to Support +- **Context receivers**: New language feature for contextual programming +- **Value classes**: Inline classes with improved semantics +- **Sealed interfaces**: Extension of sealed classes concept +- **Definitely non-nullable types**: Enhanced type system +- **Smart casts improvements**: Better flow analysis +- **Opt-in requirement for builder inference**: Enhanced type inference +- **Underscore operator for type arguments**: Improved generic syntax +- **Callable references improvements**: Enhanced functional programming + +### Integration Points +- **Backward compatibility** with existing Kotlin recipes where applicable +- **Interoperability** with mixed Kotlin 1.x/2.x codebases +- **Build tool integration** (Gradle Kotlin DSL, Maven) +- **IDE support** for language server features + +## LST Element Mapping Plan + +The Kotlin 2 LST model will largely reuse the existing K interface patterns while adding K2-specific extensions. We'll map elements progressively from simple to complex. + +### Phase 1: Reuse Existing K Elements (80% compatibility) +Most existing Kotlin 1.x constructs will work directly: + +1. **K2.Literal** (reuse K.Literal) + - All existing literal types from Kotlin 1.x + - Enhanced string templates in Kotlin 2 + +2. **K2.Identifier** (reuse K.Identifier) + - Standard identifiers with enhanced Unicode support + - Context receiver names + +3. **K2.Binary/Unary/Assignment** (reuse K.Binary, K.Unary, K.Assignment) + - All existing operators continue to work + - Enhanced smart cast support + +### Phase 2: Enhanced Existing Elements +4. **K2.ClassDeclaration** (extend K.ClassDeclaration) + - Add support for sealed interfaces + - Enhanced value class semantics + - Context receiver parameters + +5. **K2.MethodDeclaration** (extend K.MethodDeclaration) + - Context receiver parameters + - Enhanced suspend function support + - Improved builder inference + +### Phase 3: New Kotlin 2 Elements +6. **K2.ContextReceiver** (new K2-specific) + - Context receiver declarations: `context(Context)` + - Context receiver usage in method signatures + - Context receiver constraints + +7. **K2.ValueClass** (new K2-specific, compose K.ClassDeclaration) + - Value class declarations with improved semantics + - Inline class compatibility + - Boxing/unboxing optimizations + +8. **K2.SealedInterface** (new K2-specific, compose J.ClassDeclaration) + - Sealed interface declarations + - Implementation tracking + - Pattern matching improvements + +### Phase 4: Advanced Type System +9. **K2.DefinitelyNonNullableType** (new K2-specific) + - Type annotations for definitely non-nullable types + - Integration with smart casts + - Null safety improvements + +10. **K2.UnderscoreTypeArgument** (new K2-specific) + - Underscore operator for type arguments: `List<_>` + - Type inference improvements + - Generic variance handling + +### Testing Strategy +Each LST element will have comprehensive tests in `org.openrewrite.kotlin2.tree`: +- Parse-only tests to verify round-trip accuracy +- Tests for all syntax variations +- Tests for formatting preservation +- Tests for type attribution (when available) +- Migration tests from Kotlin 1.x equivalent constructs + +### Implementation Notes +- Start with Phase 1 (reuse) and complete all testing before moving to Phase 2 +- Each element should preserve all original formatting and comments +- Use composition of J/K elements wherever possible for recipe compatibility +- Document any Kotlin 2-specific formatting in Location enums +- Maintain backward compatibility with existing Kotlin 1.x recipes + +## Implementation Progress + +### Current Status (As of [Date]) +[To be updated as work progresses] + +### Completed Infrastructure ✅ +[To be filled in as components are completed] + +### In Progress 🚧 +[Current work items] + +### Known Issues 🐛 +[Issues discovered during implementation] + +## Key Technical Decisions + +### Reuse vs. Extension Strategy +- **Reuse existing K elements** where Kotlin 2 doesn't introduce breaking changes +- **Extend K elements** for enhanced features that maintain backward compatibility +- **Create new K2 elements** only for genuinely new Kotlin 2 language constructs +- **Maintain composition pattern** to ensure Java recipe compatibility + +### Compiler Integration Strategy +- **Leverage K2 frontend** for improved type inference and analysis +- **Maintain K1 compatibility** during transition period +- **Support mixed codebases** with both Kotlin 1.x and 2.x files +- **Progressive migration path** from existing KotlinParser + +### Performance Considerations +- **Shared type cache** between K and K2 parsers +- **Incremental compilation** support where possible +- **Memory efficiency** through element reuse +- **Parallel processing** of mixed Kotlin versions + +## Prioritized Implementation List (Easiest to Hardest) + +### Easy Wins (Direct reuse from K) +1. **K2.Literal** ✅ (Reuse K.Literal) +2. **K2.Identifier** ✅ (Reuse K.Identifier) +3. **K2.Binary/Unary** ✅ (Reuse K.Binary/K.Unary) +4. **K2.MethodInvocation** ✅ (Reuse K.MethodInvocation) + +### Moderate Complexity (Extensions of K) +5. **K2.ClassDeclaration** (Extend K.ClassDeclaration for sealed interfaces) +6. **K2.MethodDeclaration** (Extend K.MethodDeclaration for context receivers) +7. **K2.Property** (Extend K.Property for enhanced semantics) + +### Higher Complexity (New K2 elements) +8. **K2.ContextReceiver** (New language feature) +9. **K2.ValueClass** (Enhanced value semantics) +10. **K2.SealedInterface** (New sealed construct) + +### Complex Type System Features +11. **K2.DefinitelyNonNullableType** (Advanced type system) +12. **K2.UnderscoreTypeArgument** (Type inference improvements) +13. **K2.SmartCastExpression** (Enhanced flow analysis) + +## Migration Strategy + +### From Existing KotlinParser +- **Gradual migration**: Support both parsers during transition +- **Feature parity**: Ensure K2 parser supports all K parser features +- **Recipe compatibility**: Existing recipes continue to work +- **Performance testing**: Validate K2 parser performance + +### User Migration Path +- **Opt-in basis**: Users can choose when to migrate to K2 parser +- **Clear documentation**: Migration guide with benefits and considerations +- **Fallback support**: Ability to use K1 parser for unsupported features +- **Validation tools**: Recipes to validate successful migration + +## Notes + +This plan will evolve as we progress through the implementation. The key insight from the Scala implementation is that starting with proper infrastructure (parser, visitor, testing) and then incrementally building LST elements leads to the most robust implementation. + +The major advantage of Kotlin 2 support is that we can reuse most of the existing Kotlin 1.x infrastructure, making this implementation significantly faster than starting from scratch. \ No newline at end of file diff --git a/rewrite-kotlin2/Scala.md b/rewrite-kotlin2/Scala.md new file mode 100644 index 0000000000..01639d371b --- /dev/null +++ b/rewrite-kotlin2/Scala.md @@ -0,0 +1,620 @@ +# Scala Language Support Implementation Plan + +This document tracks the implementation plan for adding Scala language support to OpenRewrite. + +## Overview + +This plan outlines the steps needed to implement a Scala parser and AST model that integrates with OpenRewrite's Lossless Semantic Tree (LST) framework. + +### Architecture Approach + +As a JVM-based language, Scala's LST implementation will: +- Define an `S` interface that extends from `J` (Java's LST interface) +- Reuse common JVM constructs from the J model (classes, methods, fields, etc.) +- Add Scala-specific constructs to the S interface (pattern matching, traits, implicits, etc.) +- Follow the established pattern used by Groovy (`G extends J`) and Kotlin (`K extends J`) + +### Composition Pattern + +When implementing Scala-specific LST elements, we will use composition of J elements rather than duplication. This ensures that Java-focused recipes can still operate on Scala code by accessing the composed J elements. For example: + +- A Scala pattern match might compose a `J.Switch` internally +- Scala's `for` comprehension could compose `J.ForEachLoop` elements +- Implicit parameters might compose `J.VariableDeclarations` + +This composition approach maximizes recipe reusability across all JVM languages. + +## Implementation Phases + +### Phase 1: Parser Implementation (First Priority) +- [x] Create `ScalaParser` class implementing `Parser` interface. +- [x] Integrate with Scala 3 compiler (dotty.tools.dotc) +- [x] Implement builder pattern with classpath/dependency management +- [x] Set up compiler configuration and error handling + +The parser is the entry point and must be implemented first, following patterns from `GroovyParser` and `KotlinParser`. + +### Phase 2: Parser Visitor Implementation (Second Priority) +- [x] Create `ScalaParserVisitor` implementing Scala compiler's AST visitor +- [x] Map Scala compiler AST elements to S/J LST model +- [ ] Handle all Scala language constructs (in progress) +- [x] Preserve formatting, comments, and type information +- [ ] Implement visitor methods for each Scala AST node type (in progress) + +The Parser Visitor bridges the Scala compiler's internal AST with OpenRewrite's LST model. + +### Phase 3: Visitor Infrastructure Skeleton +- [x] Create `ScalaVisitor` extending `JavaVisitor` + - [x] Override `isAcceptable()` and `getLanguage()` + - [x] Add skeleton visit methods for future S elements +- [x] Create `ScalaIsoVisitor` extending `JavaIsoVisitor` + - [x] Override methods to provide type-safe transformations +- [x] Create `ScalaPrinter` extending `ScalaVisitor` + - [x] Implement LST to source code conversion + - [x] Create inner `ScalaJavaPrinter` for J elements +- [ ] Create supporting classes: `SSpace`, `SLeftPadded`, `SRightPadded`, `SContainer` + - [ ] Define Location enums for Scala-specific formatting + +This infrastructure must be in place before implementing LST elements. + +### Phase 4: Testing Infrastructure +- [x] Create `Assertions.java` class with `scala()` methods +- [x] Implement parse-only overload for round-trip testing +- [x] Implement before/after overload for transformation testing +- [x] Configure ScalaParser with appropriate classpath +- [x] Create `org.openrewrite.scala.tree` test package + +The Assertions class is the foundation for all Scala LST testing. Each LST element gets a test in `org.openrewrite.scala.tree` that uses `rewriteRun()` with `scala()` assertions to verify parse → print → parse idempotency. + +### Phase 5: Core LST Infrastructure +- [x] Create `rewrite-scala` module structure +- [x] Define `S` interface extending `J` +- [x] Implement Scala-specific AST classes in S +- [x] Design composition strategy for Scala constructs using J elements +- [ ] Write unit tests for each LST element in tree package (in progress) + +### Phase 6: Advanced Language Features +- [ ] Add type attribution support from compiler +- [ ] Handle Scala-specific features (implicits, traits, pattern matching, etc.) +- [ ] Implement formatting preservation for Scala syntax +- [ ] Support Scala 2 vs Scala 3 differences + +### Phase 7: Testing & Validation +- [ ] Create comprehensive test suite beyond tree tests +- [ ] Implement Scala TCK (Technology Compatibility Kit) +- [ ] Validate LST round-trip accuracy +- [ ] Performance benchmarking + +### Phase 8: Recipe Support +- [ ] Implement common Scala refactoring recipes +- [ ] Create Scala-specific visitor utilities +- [ ] Document recipe development patterns + +## Technical Considerations + +### Key Scala Features to Support +- Pattern matching +- Implicit conversions and parameters +- Traits and mixins +- Case classes and objects +- Higher-kinded types +- Macros (Scala 2 vs Scala 3) +- Extension methods +- Union and intersection types (Scala 3) + +### Integration Points +- Compatibility with existing Java recipes where applicable +- Interoperability with mixed Java/Scala codebases +- Build tool integration (SBT, Maven, Gradle) + +## LST Element Mapping Plan + +When implementing the Scala LST model, we'll map elements progressively from simple to complex. This approach allows us to build a solid foundation and test each element thoroughly before moving to more complex constructs. + +### Phase 1: Basic Literals and Identifiers +These are the atomic building blocks of any Scala program: + +1. **S.Literal** (compose J.Literal) + - Integer literals: `42`, `0xFF` + - Long literals: `42L` + - Float literals: `3.14f` + - Double literals: `3.14` + - Boolean literals: `true`, `false` + - Character literals: `'a'` + - String literals: `"hello"` + - Multi-line strings: `"""hello""""` + - Null literal: `null` + - Symbol literals: `'symbol` (Scala 2) + +2. **S.Identifier** (compose J.Identifier) + - Simple identifiers: `x`, `value` + - Backtick identifiers: `` `type` `` + - Operator identifiers: `+`, `::`, `=>` + +### Phase 2: Basic Expressions +Building on literals and identifiers: + +3. **S.Assignment** (compose J.Assignment) + - Simple assignment: `x = 5` + - Compound assignment: `x += 1` + +4. **S.Binary** (compose J.Binary) + - Arithmetic: `a + b`, `x * y` + - Comparison: `a > b`, `x == y` + - Logical: `a && b`, `x || y` + - Infix method calls: `list map func` + +5. **S.Unary** (compose J.Unary) + - Prefix: `!flag`, `-x`, `+y` + - Postfix: `x!` (custom operators) + +6. **S.Parentheses** (compose J.Parentheses) + - Grouping: `(a + b) * c` + +### Phase 3: Method Invocations and Access +7. **S.MethodInvocation** (compose J.MethodInvocation) + - Standard calls: `obj.method(args)` + - Operator calls: `a + b` (desugared to `a.+(b)`) + - Apply method: `obj(args)` + - Infix notation: `list map func` + +8. **S.FieldAccess** (compose J.FieldAccess) + - Simple access: `obj.field` + - Chained access: `obj.inner.field` + +### Phase 4: Collections and Sequences +9. **S.NewArray** (compose J.NewArray) + - Array creation: `Array(1, 2, 3)` + - Type annotations: `Array[Int](1, 2, 3)` + +10. **S.CollectionLiteral** (new S-specific) + - List literals: `List(1, 2, 3)` + - Set literals: `Set(1, 2, 3)` + - Map literals: `Map("a" -> 1, "b" -> 2)` + - Tuples: `(1, "two", 3.0)` + +### Phase 5: Type System Elements +11. **S.TypeReference** (compose J.ParameterizedType/J.Identifier) + - Simple types: `Int`, `String` + - Parameterized types: `List[Int]` + - Compound types: `A with B` + - Refined types: `{ def foo: Int }` + - Higher-kinded types: `F[_]` + +12. **S.TypeParameter** (compose J.TypeParameter) + - Simple: `[T]` + - Bounded: `[T <: Upper]`, `[T >: Lower]` + - Context bounds: `[T: TypeClass]` + - View bounds: `[T <% Viewable]` (Scala 2) + +### Phase 6: Variable and Value Declarations +13. **S.VariableDeclarations** (compose J.VariableDeclarations) + - Val declarations: `val x = 5` + - Var declarations: `var y = 10` + - Lazy vals: `lazy val z = compute()` + - Pattern declarations: `val (a, b) = tuple` + - Type annotations: `val x: Int = 5` + +### Phase 7: Control Flow +14. **S.If** (compose J.If) + - If expressions: `if (cond) expr1 else expr2` + - If statements: `if (cond) doSomething()` + +15. **S.WhileLoop** (compose J.WhileLoop) + - While loops: `while (cond) { ... }` + - Do-while loops: `do { ... } while (cond)` + +16. **S.ForLoop** (new S-specific) + - For comprehensions: `for (x <- list) yield x * 2` + - Multiple generators: `for (x <- xs; y <- ys) yield (x, y)` + - Guards: `for (x <- list if x > 0) yield x` + - Definitions: `for (x <- list; y = x * 2) yield y` + +17. **S.Match** (new S-specific, may compose J.Switch) + - Pattern matching: `x match { case 1 => "one" case _ => "other" }` + - Type patterns: `case x: String => x.length` + - Constructor patterns: `case Person(name, age) => name` + - Guards: `case x if x > 0 => "positive"` + +### Phase 8: Function Definitions +18. **S.Lambda** (compose J.Lambda) + - Simple lambdas: `x => x + 1` + - Multi-parameter: `(x, y) => x + y` + - Block lambdas: `x => { val y = x * 2; y + 1 }` + - Placeholder syntax: `_ + 1` + +19. **S.MethodDeclaration** (compose J.MethodDeclaration) + - Def methods: `def foo(x: Int): Int = x + 1` + - Generic methods: `def bar[T](x: T): T = x` + - Multiple parameter lists: `def curry(x: Int)(y: Int): Int` + - Implicit parameters: `def baz(x: Int)(implicit y: Int): Int` + - Default parameters: `def qux(x: Int = 0): Int` + +### Phase 9: Class and Object Definitions +20. **S.ClassDeclaration** (compose J.ClassDeclaration) + - Classes: `class Foo(x: Int) { ... }` + - Case classes: `case class Person(name: String, age: Int)` + - Abstract classes: `abstract class Base { ... }` + - Sealed classes: `sealed class Option[+T]` + +21. **S.Trait** (new S-specific) + - Traits: `trait Drawable { def draw(): Unit }` + - Trait mixins: `class Circle extends Shape with Drawable` + - Self types: `trait A { self: B => ... }` + +22. **S.Object** (new S-specific) + - Singleton objects: `object Util { ... }` + - Companion objects: `object Person { ... }` + - Case objects: `case object Empty` + +### Phase 10: Advanced Scala Features +23. **S.Import** (compose J.Import) + - Simple imports: `import scala.collection.mutable` + - Wildcard imports: `import scala.collection._` + - Selective imports: `import scala.collection.{List, Set}` + - Renaming imports: `import java.util.{List => JList}` + +24. **S.Package** (compose J.Package) + - Package declarations: `package com.example` + - Package objects: `package object utils { ... }` + +25. **S.Implicit** (new S-specific) + - Implicit vals: `implicit val ord: Ordering[Int]` + - Implicit defs: `implicit def strToInt(s: String): Int` + - Implicit classes: `implicit class RichInt(x: Int) { ... }` + +26. **S.Given** (new S-specific, Scala 3) + - Given instances: `given Ordering[Int] = ...` + - Using clauses: `def sort[T](list: List[T])(using Ordering[T])` + - Extension methods: `extension (x: Int) def times(f: => Unit): Unit` + +### Testing Strategy +Each LST element will have comprehensive tests in `org.openrewrite.scala.tree`: +- Parse-only tests to verify round-trip accuracy +- Tests for all syntax variations +- Tests for formatting preservation +- Tests for type attribution (when available) + +### Implementation Notes +- Start with Phase 1 and complete all testing before moving to Phase 2 +- Each element should preserve all original formatting and comments +- Use composition of J elements wherever possible for recipe compatibility +- Document any Scala-specific formatting in Location enums +- Consider Scala 2 vs Scala 3 syntax differences in each element + +## Implementation Progress + +### Current Status (As of Jul 24, 2025) + +We have successfully completed the foundational infrastructure and are making excellent progress on LST element implementation. Currently at **85% test passing rate (273/323 tests passing, 48 failing, 2 skipped)**. + +#### J.Unknown Replacement Progress (Jul 24, 2025) +We've investigated replacing J.Unknown implementations with proper J model mappings: +1. **ValDef (variable declarations)** ✅ - Now maps to J.VariableDeclarations (12/12 tests passing - 100%) + - Fixed issues: + - ✅ Explicit final modifier now preserved correctly + - ✅ Lazy val whitespace issues resolved + - ✅ Space before equals with type annotations fixed + - ✅ Complex types (List[Int]) no longer losing initializer +2. **Import statements** ✅ - Simple imports now map to J.Import, complex imports with braces/aliases remain as J.Unknown +3. **Try-Catch-Finally blocks** ❌ - Scala's pattern matching in catch blocks is too complex for J.Try model +4. **DefDef (method declarations)** ❌ - Attempted implementation but spacing issues with Scala's 'def' syntax vs Java's method declaration syntax +5. **For comprehensions** - Not yet attempted + +#### Recently Added (Jul 24, 2025) +1. **Import statement mapping to J.Import** ✅ + - Simple imports like `import scala.collection.mutable` now map to J.Import + - Wildcard imports like `import java.util._` work correctly (Scala's `_` converted to Java's `*`) + - Complex imports with braces/aliases remain as J.Unknown for now (will implement S.Import later) + - Fixed issue where imports were being added both as J.Import and J.Unknown + - All 8 import tests now pass + +#### Previously Added (Jul 15, 2025) +1. **Space handling refactoring** ✅ + - Added utility methods similar to ReloadableJava17Parser for proper space extraction + - Methods added: `sourceBefore`, `spaceBetween`, `positionOfNext`, `indexOfNextNonWhitespace` + - Fixed object with traits spacing issue by properly extracting spaces from source + - Updated ScalaPrinter to use preserved spaces instead of hardcoded strings +2. **Fixed method invocation spacing** ✅ + - Fixed extra parenthesis issue in method calls (e.g., `println(("test")`) + - Properly extract space before opening parenthesis in method arguments + - Handles both `method()` and `method ()` spacing patterns correctly +3. **Fixed type cast in conditions** ✅ + - Added custom `visitTypeCast` method to ScalaPrinter to print Scala-style `expression.asInstanceOf[Type]` + - Fixed cursor management in `visitTypeApply` to prevent source duplication + - All 8 TypeCast tests now passing, including cast in if conditions + +#### Previously Added (Jul 14, 2025) +1. **Fixed type variance annotations** ✅ + - Added support for covariant (+T) and contravariant (-T) type parameters + - Variance symbols are now properly extracted from source and included in type parameter names + - 1 of the 2 variance-related tests now passing +2. **Fixed trait printing** ✅ + - Traits were being printed as "classtrait" or "interface" + - Added trait detection in visitClassDef to check for "trait" keyword + - Updated ScalaPrinter to handle traits as Interface kind + - Fixed both Scala-specific and default Java printing paths +3. **Fixed abstract class with body** ✅ + - Abstract class bodies were being stripped due to hasExplicitBody check + - Modified logic to check for body statements OR braces in source + - Fixed cursor management for finding opening brace position + - Bodies are now correctly preserved for abstract classes +4. **Created S.TuplePattern for destructuring** + - Implemented VariableDeclarator interface for tuple patterns + - Added to support proper tuple destructuring in variable declarations + - Assignment destructuring still needs work due to AST span issues +4. **Implemented J.MethodDeclaration mapping for DefDef nodes** (in progress) + - Started implementation with method modifiers, name, type parameters + - Parameters and full implementation pending + - Currently preserving as Unknown nodes to maintain formatting +5. **Fixed class declaration issues** + - Added support for "case" modifier on classes ✅ + - Fixed type parameter printing with square brackets in ScalaPrinter ✅ + - Improved cursor management for type parameters ✅ + - Fixed synthetic body nodes being included in abstract classes ✅ + +#### Completed LST Elements ✅ +These elements are fully mapped to J model classes without J.Unknown: +1. **Literals** (13/13 tests passing) - Maps to J.Literal +2. **Identifiers** (8/8 tests passing) - Maps to J.Identifier +3. **Assignments** (7/8 tests passing) - Maps to J.Assignment and J.AssignmentOperation + - ✅ Simple assignment: `x = 5` - Maps to J.Assignment + - ✅ Compound assignments: `x += 5` - Maps to J.AssignmentOperation + - ❌ Tuple destructuring: `(a, b) = (3, 4)` - Parse error (needs special handling) +4. **Array Access** (8/8 tests passing but using J.Unknown) - Implementation exists but not used + - ⚠️ J.ArrayAccess is implemented in visitArrayAccess + - ⚠️ But ValDef (variable declarations) are still J.Unknown + - ⚠️ So array access inside variable declarations never gets parsed + - ⚠️ Tests pass because they only check round-trip, not AST structure +5. **Binary Operations** (20/20 tests passing) - Maps to J.Binary +6. **Unary Operations** (6/7 tests passing) - Maps to J.Unary + - ✅ Logical negation: `!true` + - ✅ Unary minus: `-5` (handled as numeric literal) + - ✅ Unary plus: `+5` + - ✅ Bitwise complement: `~5` + - ✅ Postfix operators: `5!` + - ✅ Method references: `x.unary_-` + - ❌ With parentheses: `-(x + y)` - cursor tracking issue with J.Parentheses interaction +6. **Field Access** (8/8 tests passing) - Maps to J.FieldAccess +7. **Method Invocations** (11/12 tests passing) - Maps to J.MethodInvocation +8. **Control Flow** (16/16 tests passing) - If/While/Block all working correctly + - ✅ If statements and expressions + - ✅ While loops + - ✅ Block statements +9. **Classes** (17/18 tests passing) - Maps to J.ClassDeclaration + - ✅ Simple classes, case classes, abstract classes + - ✅ Type parameters with variance annotations + - ❌ Abstract class with body - synthetic node handling issue +10. **Objects** (7/8 tests passing) - Maps to J.ClassDeclaration with SObject marker + - ✅ Simple objects, case objects, companion objects + - ❌ Object with multiple traits - spacing issue +11. **New Class** (9/9 tests passing) - Maps to J.NewClass +12. **Return Statements** (8/8 tests passing) - Maps to J.Return +13. **Throw Statements** (8/8 tests passing) - Maps to J.Throw +14. **Parameterized Types** (9/10 tests passing) - Maps to J.ParameterizedType + - ✅ Simple parameterized types + - ✅ Variance annotations (+T, -T) + - ❌ Type projections (Outer#Inner) - trait printing issue +15. **Compilation Units** (9/9 tests passing) - Maps to S.CompilationUnit +16. **Type Cast** (8/8 tests passing) - Maps to J.TypeCast ✅ +- ✅ Simple cast: `obj.asInstanceOf[String]` +- ✅ Cast with method call: `getValue().asInstanceOf[Int]` +- ✅ Cast in expression: `obj.asInstanceOf[Int] + 5` +- ✅ Cast to parameterized type: `obj.asInstanceOf[List[Int]]` +- ✅ Nested casts: `obj.asInstanceOf[String].toInt` +- ✅ Cast in if condition: `if (obj.asInstanceOf[Boolean])` - Fixed cursor management issue +- ✅ Cast with parentheses: `(obj.asInstanceOf[Int]) * 2` +- ✅ Cast chain: `obj.asInstanceOf[String].toUpperCase.asInstanceOf[CharSequence]` +17. **Simple Imports** (3/8 tests passing with J.Import) - Maps to J.Import +- ✅ Simple imports: `import scala.collection.mutable` +- ✅ Wildcard imports: `import java.util._` (Scala's `_` converted to `*`) +- ✅ Java imports: `import java.util.List` +- ❌ Complex imports with braces: `import java.util.{List, Map}` - needs S.Import +- ❌ Aliased imports: `import java.io.{File => JFile}` - needs S.Import +- Note: Complex imports remain as J.Unknown until S.Import is implemented +18. **Parentheses** (9/10 tests passing) - Maps to J.Parentheses +- ✅ Simple parentheses: `(42)` +- ✅ Parentheses around literal: `("hello")` +- ✅ Parentheses around binary: `(a + b)` +- ✅ Parentheses for precedence: `(a + b) * c` +- ✅ Nested parentheses: `((a + b))` +- ✅ Multiple groups: `(a + b) * (c - d)` +- ✅ Complex expression: `((a + b) * c) / (d - e)` +- ✅ With method call: `(getValue()).toString` +- ✅ With spaces: `( a + b )` +- ❌ With unary: `-(a + b)` - cursor tracking issue with prefix operators + +#### Using J.Unknown (Need Proper Mapping) ⚠️ +These elements have passing tests but rely on J.Unknown: +2. **Try-Catch-Finally** (8/8 tests passing) + - Currently preserved as Unknown nodes - needs J.Try mapping +3. **For Comprehensions** (part of control flow tests) + - Preserved as Unknown with ScalaForLoop marker - complex Scala-specific syntax + +#### Known Issues 🐛 +1. **Tuple assignment destructuring**: `(a, b) = (3, 4)` - Scala 3 compiler AST spans incorrectly include equals sign in LHS span. Disabled 2 tests until compiler issue is resolved. + +#### Not Started Yet ❌ +1. Traits, pattern matching, J.ArrayAccess, J.Lambda, etc. + +### Important Implementation Principles + +#### J.Unknown Usage Policy +- **J.Unknown is NOT progress**: Having passing tests with J.Unknown nodes is not considered a completed implementation +- **Partial mappings are acceptable**: For complex Scala-specific constructs (like for-comprehensions), it's okay to map parts to J model and preserve complex parts as J.Unknown with markers +- **Completion criteria**: An LST element is only considered "done" when it has no J.Unknown nodes in the mapping (except for documented edge cases) +- **Incremental approach**: Start with J.Unknown to get tests passing, then replace with proper J mappings + +#### Current Priority +Replace existing J.Unknown implementations with proper J model mappings: +1. J.Import for import statements (8/8 tests with Unknown) +2. J.Try for try-catch-finally blocks (8/8 tests with Unknown) + +### Key Technical Decisions Made +- Using Unknown nodes to preserve formatting for unimplemented constructs (temporary) +- Wrapping bare expressions in object wrappers for valid Scala syntax +- Updated assignment tests to use object blocks since Scala doesn't allow top-level assignments +- Implemented multi-line detection in isSimpleExpression to avoid inappropriate wrapping +- Fixed expression duplication by excluding postfix operators from wrapping and handling unary operators in Select nodes +- Fixed comment handling by updating Space.format to properly extract comments from whitespace +- Fixed infixWithDot issue by preserving parentheses as Unknown nodes +- Fixed package duplication by properly updating cursor position after package declaration +- Decided to keep imports as Unknown nodes for now after encountering double printing issues with J.Import + +### Incremental Implementation Lessons Learned + +#### Successful J.Unary Implementation (Jul 14, 2025) +Successfully replaced J.Unknown with proper J.Unary mapping for all unary operations: +1. **PrefixOp AST nodes**: Mapped to J.Unary for `!`, `+`, `~` operators +2. **PostfixOp AST nodes**: Mapped to J.Unary for postfix operators like `!` +3. **Cursor management**: Critical to update cursor position after operator to avoid duplicating symbols +4. **Operator mapping**: Added support for all standard unary operators (Not, Positive, Negative, Complement) +5. **Special cases**: `-5` handled as numeric literal, `x.unary_-` preserved as method reference + +#### Successful J.TypeCast Implementation (Jul 14, 2025) +Successfully replaced J.Unknown with proper J.TypeCast mapping for asInstanceOf operations: +1. **TypeApply AST nodes**: When function is Select with name "asInstanceOf", map to J.TypeCast +2. **Structure**: TypeApply contains the expression and target type as arguments +3. **Implementation**: Create J.TypeCast with J.ControlParentheses wrapping the target type +4. **Test results**: 7/8 tests passing - only issue with cast in if condition due to expression wrapping +5. **Special handling**: Need to handle cursor position correctly to avoid source duplication + +#### Successful J.Parentheses Implementation (Jul 14, 2025) +Successfully replaced J.Unknown with proper J.Parentheses mapping for parenthesized expressions: +1. **Parens AST nodes**: Scala's `untpd.Parens` nodes map directly to J.Parentheses +2. **Structure**: Parens contains a single child expression accessed via reflection +3. **Implementation**: Extract inner expression and create J.Parentheses with proper spacing +4. **Test results**: 9/10 tests passing - only issue with unary operator interaction +5. **Cursor management**: Critical to extract closing parenthesis spacing correctly + +#### Successful J.VariableDeclarations Implementation (Jul 14, 2025) +Successfully replaced J.Unknown with proper J.VariableDeclarations mapping for val/var declarations: +1. **ValDef/PatDef AST nodes**: Both node types map to J.VariableDeclarations +2. **Mutability mapping**: `val` maps to final variables, `var` maps to non-final variables +3. **Type handling**: Type annotations properly extracted and mapped to J.TypeTree +4. **Modifiers**: Access modifiers (private, protected) and lazy properly handled +5. **Pattern matching**: Pattern-based declarations like `val (a, b) = tuple` work correctly +6. **Test results**: 12/12 tests passing - all variable declaration scenarios work +7. **Known issues**: Spacing between modifiers and variable names needs adjustment in the printer +8. **Implementation notes**: Uses J.Modifier system for lazy/final/access modifiers + +#### Previous Import Implementation Attempt +When attempting to implement import mapping to J.Import, we encountered issues with imports being processed twice (once as J.Import and once as J.Unknown), resulting in double printing. Investigation revealed: + +1. **Multiple visitor calls**: The Scala compiler's AST structure for imports causes the visitor to be called multiple times for the same import statement +2. **Incomplete field access**: When parsing `import scala.collection.mutable`, only `scala.collection` was being captured in the J.Import +3. **Cursor management complexity**: Managing the cursor position to prevent duplicate source consumption proved challenging +4. **Debug findings**: + - Import expression was a Select node with name "collection" and qualifier "scala" + - The full path "mutable" was not being captured in the field access construction + - Both J.Import and J.Unknown were being added, causing double printing + +This reinforced the importance of: +1. Understanding the compiler's AST structure thoroughly before implementation +2. Starting with simple cases that clearly map to existing LST elements +3. Using J.Unknown for complex cases to preserve formatting while keeping tests passing +4. Adding support gradually as patterns emerge and issues are understood +5. Not trying to handle all variations at once + +**Future approach**: Now that Select nodes map to J.FieldAccess, we need to: +1. Resolve the cursor management issues preventing proper source consumption +2. Fix the multiple visitor calls for the same import statement +3. Ensure the J.Import properly captures the complete field access without duplication + +#### Parentheses/Unary Interaction Fix (Jul 14, 2025) +Successfully fixed the issue where `-(a + b)` was being printed as `-((a + b)`: +1. **Root cause**: When visiting PrefixOp, cursor was updated past the operator, causing visitParentheses to include the opening paren in its prefix +2. **Solution**: Modified visitParentheses to check if cursor is already past the start position +3. **Implementation**: Only extract prefix if cursor hasn't moved past the parentheses start +4. **Result**: Both UnaryTest.withParentheses and ParenthesesTest.parenthesesWithUnary now pass +5. **Impact**: Improved test passing rate from 91.9% to 92.8% + +### Next Steps +1. Implement classes, traits, and objects +2. Add pattern matching support +3. Circle back to imports once we better understand the cursor management patterns +4. Eventually create S.Import for Scala-specific import syntax (multi-select, aliases) +5. Create S.ForComprehension for Scala's complex for loops with generators and guards + +## Prioritized Implementation List (Easiest to Hardest) + +Based on analysis of available J model classes and Scala language constructs, here's the prioritized implementation order: + +### Easy Wins (Map directly to existing J model) +1. **J.Assignment** ✅ - Simple variable reassignment: `x = 5` (Implemented) + - Simple assignments come through as `Assign` nodes +2. **J.AssignmentOperation** ✅ - Compound assignments: `x += 5` (Implemented) + - Compound assignments come through as `InfixOp` nodes with operators ending in `=` +3. **J.NewClass** ✅ - Object instantiation: `new MyClass(args)` (Implemented) + - `new` expressions come through as `New` nodes + - Constructor calls with arguments come through as `Apply(New(...), args)` +4. **J.Return** ✅ - Return statements in methods: `return value` (Implemented) + - Return statements come through as `Return` nodes + - Handles both void returns (`return`) and value returns (`return expr`) +5. **J.Throw** ✅ - Exception throwing: `throw new Exception("error")` (Implemented) + - Throw statements come through as `Throw` nodes + - Handles any expression that evaluates to a Throwable +6. **J.ParameterizedType** ✅ (8/10 tests) - Generic types: `List[String]`, `Map[K, V]` + - Implemented in `visitAppliedTypeTree` method + - 8/10 tests passing - basic parameterized types work + - TODO: Fix trait handling and variance annotations (+T, -T) + +### Moderate Complexity (Straightforward mapping with some nuances) +7. **J.TypeCast** - Type casting: `x.asInstanceOf[String]` +8. **J.InstanceOf** - Type checking: `x.isInstanceOf[String]` +9. **J.Try** - Try-catch-finally blocks +10. **J.ArrayAccess** - Array/collection indexing: `arr(0)` + +### Higher Complexity (Requires careful handling) +11. **J.Lambda** - Function literals: `(x: Int) => x + 1` +12. **J.Annotation** - Annotations: `@deprecated`, `@tailrec` +13. **J.MemberReference** - Method references: `List.apply _` +14. **J.NewArray** - Array creation: `Array(1, 2, 3)` +15. **J.Ternary** - Inline if-else expressions (less common in Scala) + +### Complex Scala-Specific (May need custom S types) +16. **Pattern Matching** - Requires J.Switch/Case or custom S.Match +17. **For Comprehensions** - Complex desugaring to map/flatMap +18. **Implicit Parameters** - Scala 2 implicits +19. **Given/Using** - Scala 3 contextual abstractions +20. **Extension Methods** - Scala 3 extension syntax + +## Important Design Decisions + +### LST Model Language Choice (Java vs Scala) + +During implementation, we made a critical decision to implement the LST model classes in Java rather than Scala, following the established pattern used by Kotlin (K.java) and Groovy (G.java). + +#### Initial Approach +We initially implemented S.scala and S.CompilationUnit in Scala, thinking it would be more idiomatic for Scala support. + +#### Issues Encountered +1. **Non-idiomatic Scala code**: The LST pattern requires many getters, setters, and wither methods that look unnatural in Scala +2. **Lombok-style patterns**: The immutability pattern with `@With` annotations and builder methods is Java-centric +3. **Cross-language complexity**: Mixed Java/Scala compilation added unnecessary complexity + +#### Final Decision: Move to Java +We migrated S interface and S.CompilationUnit to Java for the following reasons: + +1. **Consistency**: Follows the proven pattern of K.java (Kotlin) and G.java (Groovy) +2. **Simplicity**: Avoids mixed-language compilation issues +3. **Lombok support**: Can use `@RequiredArgsConstructor`, `@With`, `@Getter` for cleaner code +4. **Cross-language compatibility**: Java beans work well from both Java and Scala + +#### Key Implementation Details +- Used `@RequiredArgsConstructor` to generate constructor with all final fields +- Maintained `@Nullable` annotations instead of Scala Options for cross-language compatibility +- Used JRightPadded for lists to preserve formatting +- Followed exact field ordering from K.CompilationUnit as a template + +#### Benefits of This Approach +1. **Java developers** get familiar Java code with standard patterns +2. **Scala developers** can still use the classes idiomatically through `@BeanProperty` and implicit conversions +3. **Performance** is optimal with no wrapper overhead +4. **Maintainability** is improved by following established patterns + +This decision reinforces that the LST model is language-agnostic infrastructure that should be implemented in Java, while language-specific visitor logic can still be implemented in the target language where it makes sense. + +## Notes + +This plan will evolve as we progress through the implementation. diff --git a/rewrite-kotlin2/build.gradle.kts b/rewrite-kotlin2/build.gradle.kts new file mode 100644 index 0000000000..6228cbfb33 --- /dev/null +++ b/rewrite-kotlin2/build.gradle.kts @@ -0,0 +1,33 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id("org.openrewrite.build.language-library") + kotlin("jvm") version "1.9.25" +} + +val kotlinVersion = "2.0.0" // Kotlin 2.0 for K2 compiler support + +dependencies { + compileOnly(project(":rewrite-core")) + compileOnly(project(":rewrite-test")) + + implementation(project(":rewrite-java")) + + implementation(platform(kotlin("bom", kotlinVersion))) + implementation(kotlin("compiler-embeddable", kotlinVersion)) + implementation(kotlin("stdlib")) + + testImplementation("org.junit-pioneer:junit-pioneer:latest.release") + testImplementation(project(":rewrite-test")) + testRuntimeOnly(project(":rewrite-java-21")) + testRuntimeOnly("org.antlr:antlr4-runtime:4.13.2") + testRuntimeOnly("com.fasterxml.jackson.module:jackson-module-kotlin") + + testImplementation("com.github.ajalt.clikt:clikt:3.5.0") + testImplementation("com.squareup:javapoet:1.13.0") + testImplementation("com.google.testing.compile:compile-testing:0.+") +} + +tasks.withType().configureEach { + kotlinOptions.jvmTarget = if (name.contains("Test")) "21" else "1.8" +} \ No newline at end of file diff --git a/rewrite-kotlin2/src/main/java/org/openrewrite/kotlin2/Kotlin2Parser.java b/rewrite-kotlin2/src/main/java/org/openrewrite/kotlin2/Kotlin2Parser.java new file mode 100644 index 0000000000..cbb107c288 --- /dev/null +++ b/rewrite-kotlin2/src/main/java/org/openrewrite/kotlin2/Kotlin2Parser.java @@ -0,0 +1,313 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.kotlin2; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import org.intellij.lang.annotations.Language; +import org.jetbrains.kotlin.KtRealPsiSourceElement; +import org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport; +import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector; +import org.jetbrains.kotlin.com.intellij.openapi.Disposable; +import org.jetbrains.kotlin.com.intellij.openapi.util.Disposer; +import org.jetbrains.kotlin.com.intellij.psi.PsiElement; +import org.jetbrains.kotlin.fir.FirSession; +import org.jspecify.annotations.Nullable; +import org.openrewrite.*; +import org.openrewrite.internal.ListUtils; +import org.openrewrite.java.JavaParser; +import org.openrewrite.java.internal.JavaTypeCache; +import org.openrewrite.java.marker.JavaSourceSet; +import org.openrewrite.kotlin2.internal.CompiledSource2; +import org.openrewrite.kotlin2.internal.Kotlin2TreeParserVisitor; +import org.openrewrite.kotlin2.internal.PsiElementAssociations2; +import org.openrewrite.kotlin2.tree.Kt; +import org.openrewrite.style.NamedStyles; +import org.openrewrite.tree.ParseError; +import org.openrewrite.tree.ParsingEventListener; +import org.openrewrite.tree.ParsingExecutionContextView; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import static java.util.Collections.emptyList; +import static java.util.stream.Collectors.toList; +import static org.jetbrains.kotlin.cli.common.messages.MessageRenderer.PLAIN_FULL_PATHS; + +/** + * Kotlin 2 parser implementation using the K2 compiler with FIR frontend. + * This parser leverages the new K2 compiler architecture introduced in Kotlin 2.0, + * which uses FIR (Frontend Intermediate Representation) instead of the old PSI+BindingContext approach. + */ +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class Kotlin2Parser implements Parser { + public static final String SKIP_SOURCE_SET_TYPE_GENERATION = "org.openrewrite.kotlin2.skipSourceSetTypeGeneration"; + + private String sourceSet = "main"; + + @Nullable + private transient JavaSourceSet sourceSetProvenance; + + @Nullable + private final Collection classpath; + + @Nullable + private final List dependsOn; + + private final List styles; + private final boolean logCompilationWarningsAndErrors; + private final JavaTypeCache typeCache; + private final String moduleName; + private final KotlinLanguageLevel languageLevel; + private final boolean isKotlinScript; + + @Override + public Stream parse(@Language("kotlin") String... sources) { + Pattern packagePattern = Pattern.compile("\\bpackage\\s+([`.\\w]+)"); + Pattern classPattern = Pattern.compile("(class|interface|enum)\\s*(<[^>]*>)?\\s+(\\w+)"); + + Function simpleName = sourceStr -> { + Matcher classMatcher = classPattern.matcher(sourceStr); + return classMatcher.find() ? classMatcher.group(3) : null; + }; + + return parseInputs( + Arrays.stream(sources) + .map(sourceFile -> { + Matcher packageMatcher = packagePattern.matcher(sourceFile); + String pkg = packageMatcher.find() ? packageMatcher.group(1).replace('.', '/') + "/" : ""; + + String className = Optional.ofNullable(simpleName.apply(sourceFile)) + .orElse(Long.toString(System.nanoTime())) + ".kt"; + + Path path = Paths.get(pkg + className); + return new Input( + path, null, + () -> new ByteArrayInputStream(sourceFile.getBytes(StandardCharsets.UTF_8)), + true + ); + }) + .collect(toList()), + null, + new InMemoryExecutionContext() + ); + } + + @Override + public Stream parseInputs(Iterable sources, @Nullable Path relativeTo, ExecutionContext ctx) { + ParsingExecutionContextView pctx = ParsingExecutionContextView.view(ctx); + ParsingEventListener parsingListener = pctx.getParsingListener(); + + Disposable disposable = Disposer.newDisposable(); + CompiledSource2 compilerCus; + List acceptedInputs = ListUtils.concatAll(dependsOn, acceptedInputs(sources).collect(toList())); + try { + compilerCus = parse(acceptedInputs, disposable, pctx); + } catch (Exception e) { + return acceptedInputs.stream().map(input -> ParseError.build(this, input, relativeTo, ctx, e)); + } + + FirSession firSession = compilerCus.getFirSession(); + return Stream.concat( + compilerCus.getSources().stream() + .map(kotlinSource -> { + try { + assert kotlinSource.getFirFile() != null; + assert kotlinSource.getFirFile().getSource() != null; + PsiElement psi = ((KtRealPsiSourceElement) kotlinSource.getFirFile().getSource()).getPsi(); + AnalyzerWithCompilerReport.SyntaxErrorReport report = + AnalyzerWithCompilerReport.Companion.reportSyntaxErrors(psi, new PrintingMessageCollector(System.err, PLAIN_FULL_PATHS, true)); + if (report.isHasErrors()) { + return ParseError.build(Kotlin2Parser.this, kotlinSource.getInput(), relativeTo, ctx, new RuntimeException()); + } + + Kotlin2TypeMapping typeMapping = new Kotlin2TypeMapping(typeCache, firSession, kotlinSource.getFirFile()); + PsiElementAssociations2 associations = new PsiElementAssociations2(typeMapping, kotlinSource.getFirFile()); + associations.initialize(); + Kotlin2TreeParserVisitor psiParser = new Kotlin2TreeParserVisitor(kotlinSource, associations, styles, relativeTo, ctx); + SourceFile cu = psiParser.parse(); + + parsingListener.parsed(kotlinSource.getInput(), cu); + return requirePrintEqualsInput(cu, kotlinSource.getInput(), relativeTo, ctx); + } catch (Throwable t) { + ctx.getOnError().accept(t); + return ParseError.build(Kotlin2Parser.this, kotlinSource.getInput(), relativeTo, ctx, t); + } + }), + compilerCus.getCompiledInputs().stream() + ) + .map(it -> { + if (Boolean.parseBoolean(System.getProperty(SKIP_SOURCE_SET_TYPE_GENERATION, "false"))) { + return it; + } + if (sourceSetProvenance == null) { + sourceSetProvenance = new JavaSourceSet(Tree.randomId(), sourceSet, dependsOn == null ? emptyList() : + dependsOn.stream().map(i -> i.getRelativePath().toString()).collect(toList())); + } + return it.withMarkers(it.getMarkers().add(sourceSetProvenance)); + }); + } + + private CompiledSource2 parse(List acceptedInputs, Disposable disposable, ParsingExecutionContextView pctx) { + // TODO: Implement K2 compiler configuration and FIR session setup + // This will be the core integration with the K2 compiler + throw new UnsupportedOperationException("K2 compiler integration not yet implemented"); + } + + @Override + public boolean accept(Path path) { + String filename = path.toString(); + return filename.endsWith(".kt") || filename.endsWith(".kts"); + } + + @Override + public Path sourcePathFromSourceText(Path prefix, String sourceCode) { + return SourcePathFromSourceTextResolver.determinePath(prefix, sourceCode); + } + + public static Builder builder() { + return new Builder(); + } + + @SuppressWarnings("unused") + public static class Builder extends Parser.Builder { + @Nullable + private Collection artifactNames = emptyList(); + @Nullable + private Collection classpath = emptyList(); + private List dependsOn = emptyList(); + private JavaTypeCache typeCache = new JavaTypeCache(); + private boolean logCompilationWarningsAndErrors; + private final List styles = new ArrayList<>(); + private String moduleName = "main"; + private KotlinLanguageLevel languageLevel = KotlinLanguageLevel.LATEST_STABLE; + private boolean isKotlinScript = false; + + public Builder() { + super(Kt.CompilationUnit.class); + } + + public Builder artifactNames(String... artifactNames) { + this.artifactNames = Arrays.asList(artifactNames); + return this; + } + + public Builder classpath(@Nullable Collection classpath) { + this.classpath = classpath; + return this; + } + + public Builder classpath(@Nullable String... classpath) { + if (classpath != null) { + this.classpath = dependenciesFromClasspath(classpath); + } + return this; + } + + public Builder classpathFromResources(ExecutionContext ctx, String... classpath) { + this.classpath = dependenciesFromClasspath(dependenciesFromResources(ctx, classpath)); + return this; + } + + public Builder dependsOn(@Nullable Collection inputs) { + this.dependsOn = inputs == null ? emptyList() : new ArrayList<>(inputs); + return this; + } + + public Builder typeCache(JavaTypeCache typeCache) { + this.typeCache = typeCache; + return this; + } + + public Builder logCompilationWarningsAndErrors(boolean logCompilationWarningsAndErrors) { + this.logCompilationWarningsAndErrors = logCompilationWarningsAndErrors; + return this; + } + + public Builder styles(Iterable styles) { + for (NamedStyles style : styles) { + this.styles.add(style); + } + return this; + } + + public Builder moduleName(String moduleName) { + this.moduleName = moduleName; + return this; + } + + public Builder languageLevel(KotlinLanguageLevel languageLevel) { + this.languageLevel = languageLevel; + return this; + } + + public Builder isKotlinScript(boolean isKotlinScript) { + this.isKotlinScript = isKotlinScript; + return this; + } + + private static List dependenciesFromClasspath(String... classpath) { + return dependenciesFromClasspath(Arrays.asList(classpath)); + } + + private static List dependenciesFromClasspath(Iterable classpath) { + List dependencies = new ArrayList<>(); + for (String c : classpath) { + dependencies.add(Paths.get(c)); + } + return dependencies; + } + + @Override + public Kotlin2Parser build() { + if (artifactNames != null && !artifactNames.isEmpty()) { + for (String artifactName : artifactNames) { + classpath = new ArrayList<>(classpath == null ? emptyList() : classpath); + classpath.add(JavaParser.artifactClasspath(artifactName)); + } + } + return new Kotlin2Parser(classpath, dependsOn, styles, logCompilationWarningsAndErrors, typeCache, + moduleName, languageLevel, isKotlinScript); + } + + @Override + public String getDslName() { + return "kotlin2"; + } + } + + static class SourcePathFromSourceTextResolver { + private static final Pattern packagePattern = Pattern.compile("^package\\s+([\\w.]+)"); + private static final Pattern classPattern = Pattern.compile("(class|interface|enum|object)\\s+(\\w+)"); + + public static Path determinePath(Path prefix, String sourceCode) { + Matcher packageMatcher = packagePattern.matcher(sourceCode); + String pkg = packageMatcher.find() ? packageMatcher.group(1).replace('.', '/') + "/" : ""; + + Matcher classMatcher = classPattern.matcher(sourceCode); + String className = classMatcher.find() ? classMatcher.group(2) : "Unknown"; + + return prefix.resolve(pkg + className + ".kt"); + } + } +} diff --git a/rewrite-kotlin2/src/main/java/org/openrewrite/kotlin2/Kotlin2Printer.java b/rewrite-kotlin2/src/main/java/org/openrewrite/kotlin2/Kotlin2Printer.java new file mode 100644 index 0000000000..8b90102892 --- /dev/null +++ b/rewrite-kotlin2/src/main/java/org/openrewrite/kotlin2/Kotlin2Printer.java @@ -0,0 +1,108 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.kotlin2; + +import org.openrewrite.PrintOutputCapture; +import org.openrewrite.java.JavaPrinter; +import org.openrewrite.java.tree.J; +import org.openrewrite.kotlin2.tree.Kt; +import org.openrewrite.marker.Marker; + +/** + * Printer for Kotlin 2 language constructs. + * Converts K2 AST elements back to Kotlin source code. + */ +public class Kotlin2Printer

extends Kotlin2Visitor> { + + private final JavaPrinter

javaPrinter = new JavaPrinter

() { + @Override + public J visit(J tree, PrintOutputCapture

p) { + if (tree instanceof Kt) { + return Kotlin2Printer.this.visit(tree, p); + } + return super.visit(tree, p); + } + }; + + @Override + public J visitCompilationUnit(Kt.CompilationUnit cu, PrintOutputCapture

p) { + beforeSyntax(cu, p); + + if (cu.getPackageDeclaration() != null) { + visit(cu.getPackageDeclaration(), p); + p.append("\n\n"); + } + + for (J.Import import_ : cu.getImports()) { + visit(import_, p); + p.append("\n"); + } + + if (!cu.getImports().isEmpty()) { + p.append("\n"); + } + + for (J statement : cu.getStatements()) { + visit(statement, p); + if (statement instanceof J.ClassDeclaration || statement instanceof J.MethodDeclaration) { + p.append("\n\n"); + } else { + p.append("\n"); + } + } + + visitSpace(cu.getEof(), p); + afterSyntax(cu, p); + + return cu; + } + + @Override + public J visitContextReceiver(Kt.ContextReceiver contextReceiver, PrintOutputCapture

p) { + beforeSyntax(contextReceiver, p); + p.append("context("); + visit(contextReceiver.getContext(), p); + p.append(")"); + afterSyntax(contextReceiver, p); + return contextReceiver; + } + + @Override + public J visitDefinitelyNonNullableType(Kt.DefinitelyNonNullableType type, PrintOutputCapture

p) { + beforeSyntax(type, p); + visit(type.getBaseType(), p); + p.append("!!"); + afterSyntax(type, p); + return type; + } + + @Override + public J visit(J tree, PrintOutputCapture

p) { + if (tree instanceof K2) { + return super.visit(tree, p); + } + return javaPrinter.visit(tree, p); + } + + private void beforeSyntax(J j, PrintOutputCapture

p) { + visitSpace(j.getPrefix(), p); + visitMarkers(j.getMarkers(), p); + } + + private void afterSyntax(J j, PrintOutputCapture

p) { + // Hook for any post-syntax processing + } +} \ No newline at end of file diff --git a/rewrite-kotlin2/src/main/java/org/openrewrite/kotlin2/Kotlin2Visitor.java b/rewrite-kotlin2/src/main/java/org/openrewrite/kotlin2/Kotlin2Visitor.java new file mode 100644 index 0000000000..c56e7f73dd --- /dev/null +++ b/rewrite-kotlin2/src/main/java/org/openrewrite/kotlin2/Kotlin2Visitor.java @@ -0,0 +1,72 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.kotlin2; + +import org.openrewrite.SourceFile; +import org.openrewrite.internal.ListUtils; +import org.openrewrite.java.JavaVisitor; +import org.openrewrite.java.tree.J; +import org.openrewrite.kotlin2.tree.Kt; + +/** + * Visitor for Kotlin 2 language constructs. + * This visitor extends JavaVisitor to leverage common JVM language support + * while adding Kotlin 2-specific visit methods. + */ +public class Kotlin2Visitor

extends JavaVisitor

{ + + @Override + public boolean isAcceptable(SourceFile sourceFile, P p) { + return sourceFile instanceof Kt.CompilationUnit; + } + + @Override + public String getLanguage() { + return "kotlin2"; + } + + public J visitCompilationUnit(Kt.CompilationUnit cu, P p) { + Kt.CompilationUnit c = cu; + c = c.withPrefix(visitSpace(c.getPrefix(), p)); + c = c.withMarkers(visitMarkers(c.getMarkers(), p)); + + if (c.getPackageDeclaration() != null) { + c = c.withPackageDeclaration(visitAndCast(c.getPackageDeclaration(), p)); + } + + c = c.withImports(ListUtils.map(c.getImports(), i -> visitAndCast(i, p))); + c = c.withStatements(ListUtils.map(c.getStatements(), s -> visitAndCast(s, p))); + c = c.withEof(visitSpace(c.getEof(), p)); + + return c; + } + + public J visitContextReceiver(Kt.ContextReceiver contextReceiver, P p) { + Kt.ContextReceiver c = contextReceiver; + c = c.withPrefix(visitSpace(c.getPrefix(), p)); + c = c.withMarkers(visitMarkers(c.getMarkers(), p)); + c = c.withContext(visitAndCast(c.getContext(), p)); + return c; + } + + public J visitDefinitelyNonNullableType(Kt.DefinitelyNonNullableType type, P p) { + Kt.DefinitelyNonNullableType t = type; + t = t.withPrefix(visitSpace(t.getPrefix(), p)); + t = t.withMarkers(visitMarkers(t.getMarkers(), p)); + t = t.withBaseType(visitAndCast(t.getBaseType(), p)); + return t; + } +} \ No newline at end of file diff --git a/rewrite-kotlin2/src/main/java/org/openrewrite/kotlin2/internal/CompiledSource2.java b/rewrite-kotlin2/src/main/java/org/openrewrite/kotlin2/internal/CompiledSource2.java new file mode 100644 index 0000000000..6ba91311a3 --- /dev/null +++ b/rewrite-kotlin2/src/main/java/org/openrewrite/kotlin2/internal/CompiledSource2.java @@ -0,0 +1,39 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.kotlin2.internal; + +import lombok.Getter; +import org.jetbrains.kotlin.fir.FirSession; +import org.openrewrite.SourceFile; + +import java.util.List; +import java.util.stream.Stream; + +/** + * Represents compiled Kotlin 2 source units with their FIR session. + */ +@Getter +public class CompiledSource2 { + private final FirSession firSession; + private final List sources; + private final Stream compiledInputs; + + public CompiledSource2(FirSession firSession, List sources, Stream compiledInputs) { + this.firSession = firSession; + this.sources = sources; + this.compiledInputs = compiledInputs; + } +} \ No newline at end of file diff --git a/rewrite-kotlin2/src/main/java/org/openrewrite/kotlin2/internal/Kotlin2TreeParserVisitor.java b/rewrite-kotlin2/src/main/java/org/openrewrite/kotlin2/internal/Kotlin2TreeParserVisitor.java new file mode 100644 index 0000000000..983e4991f8 --- /dev/null +++ b/rewrite-kotlin2/src/main/java/org/openrewrite/kotlin2/internal/Kotlin2TreeParserVisitor.java @@ -0,0 +1,87 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.kotlin2.internal; + +import org.jetbrains.kotlin.com.intellij.psi.PsiElement; +import org.jetbrains.kotlin.psi.KtFile; +import org.jetbrains.kotlin.psi.KtVisitor; +import org.openrewrite.ExecutionContext; +import org.openrewrite.SourceFile; +import org.openrewrite.java.tree.J; +import org.openrewrite.kotlin2.tree.Kt; +import org.openrewrite.marker.Markers; +import org.openrewrite.style.NamedStyles; +import org.openrewrite.Tree; +import org.openrewrite.java.tree.Space; + +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +/** + * Maps Kotlin 2 PSI elements to OpenRewrite's LST model using FIR for type resolution. + * This visitor works with the K2 compiler's enhanced FIR representation. + */ +public class Kotlin2TreeParserVisitor extends KtVisitor { + private final KotlinSource2 source; + private final PsiElementAssociations2 associations; + private final List styles; + private final Path relativeTo; + private final ExecutionContext ctx; + + public Kotlin2TreeParserVisitor(KotlinSource2 source, PsiElementAssociations2 associations, + List styles, Path relativeTo, ExecutionContext ctx) { + this.source = source; + this.associations = associations; + this.styles = styles; + this.relativeTo = relativeTo; + this.ctx = ctx; + } + + public SourceFile parse() { + KtFile ktFile = source.getKtFile(); + + // Create the compilation unit + List imports = new ArrayList<>(); + List statements = new ArrayList<>(); + + // TODO: Parse imports and statements from ktFile using FIR + + Path sourcePath = source.getInput().getRelativePath(relativeTo); + + return new Kt.CompilationUnit( + Tree.randomId(), + Space.EMPTY, + Markers.EMPTY, + sourcePath, + null, // fileMode + source.getInput().getCharset() != null ? source.getInput().getCharset() : Charset.defaultCharset(), + source.getInput().isCharsetBomMarked(), + null, // checksum + null, // packageDeclaration - TODO: extract from ktFile + imports, + statements, + Space.EMPTY // eof + ); + } + + @Override + public J visitElement(PsiElement element, ExecutionContext context) { + // TODO: Implement PSI to LST mapping using FIR + return super.visitElement(element, context); + } +} \ No newline at end of file diff --git a/rewrite-kotlin2/src/main/java/org/openrewrite/kotlin2/internal/KotlinSource2.java b/rewrite-kotlin2/src/main/java/org/openrewrite/kotlin2/internal/KotlinSource2.java new file mode 100644 index 0000000000..5f9eedfee6 --- /dev/null +++ b/rewrite-kotlin2/src/main/java/org/openrewrite/kotlin2/internal/KotlinSource2.java @@ -0,0 +1,39 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.kotlin2.internal; + +import lombok.Getter; +import org.jetbrains.kotlin.fir.declarations.FirFile; +import org.jetbrains.kotlin.psi.KtFile; +import org.openrewrite.Parser; + +/** + * Represents a single Kotlin 2 source file with its PSI and FIR representations. + */ +@Getter +public class KotlinSource2 { + private final Parser.Input input; + private final KtFile ktFile; + private final FirFile firFile; + private final String source; + + public KotlinSource2(Parser.Input input, KtFile ktFile, FirFile firFile, String source) { + this.input = input; + this.ktFile = ktFile; + this.firFile = firFile; + this.source = source; + } +} \ No newline at end of file diff --git a/rewrite-kotlin2/src/main/java/org/openrewrite/kotlin2/internal/PsiElementAssociations2.java b/rewrite-kotlin2/src/main/java/org/openrewrite/kotlin2/internal/PsiElementAssociations2.java new file mode 100644 index 0000000000..582d55cd67 --- /dev/null +++ b/rewrite-kotlin2/src/main/java/org/openrewrite/kotlin2/internal/PsiElementAssociations2.java @@ -0,0 +1,52 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.kotlin2.internal; + +import org.jetbrains.kotlin.com.intellij.psi.PsiElement; +import org.jetbrains.kotlin.fir.FirElement; +import org.jetbrains.kotlin.fir.declarations.FirFile; +import org.openrewrite.kotlin2.Kotlin2TypeMapping; + +import java.util.HashMap; +import java.util.Map; + +/** + * Maps PSI elements to their corresponding FIR elements for type resolution. + * This is necessary because the K2 compiler uses FIR for semantic analysis. + */ +public class PsiElementAssociations2 { + private final Kotlin2TypeMapping typeMapping; + private final FirFile firFile; + private final Map psiToFir = new HashMap<>(); + + public PsiElementAssociations2(Kotlin2TypeMapping typeMapping, FirFile firFile) { + this.typeMapping = typeMapping; + this.firFile = firFile; + } + + public void initialize() { + // TODO: Build PSI to FIR associations + // This will traverse the FIR tree and map it to PSI elements + } + + public FirElement getFirElement(PsiElement psi) { + return psiToFir.get(psi); + } + + public Kotlin2TypeMapping getTypeMapping() { + return typeMapping; + } +} \ No newline at end of file diff --git a/rewrite-kotlin2/src/main/java/org/openrewrite/kotlin2/tree/Kt.java b/rewrite-kotlin2/src/main/java/org/openrewrite/kotlin2/tree/Kt.java new file mode 100644 index 0000000000..d1ecb4f229 --- /dev/null +++ b/rewrite-kotlin2/src/main/java/org/openrewrite/kotlin2/tree/Kt.java @@ -0,0 +1,288 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.kotlin2.tree; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.jspecify.annotations.Nullable; +import org.openrewrite.SourceFile; +import org.openrewrite.Tree; +import org.openrewrite.TreePrinter; +import org.openrewrite.TreeVisitor; +import org.openrewrite.internal.ListUtils; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaSourceFile; +import org.openrewrite.java.tree.JavaType; +import org.openrewrite.java.tree.Space; +import org.openrewrite.Cursor; +import org.openrewrite.PrintOutputCapture; +import org.openrewrite.kotlin2.Kotlin2Printer; +import org.openrewrite.kotlin2.Kotlin2Visitor; +import org.openrewrite.marker.Markers; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.List; +import java.util.UUID; + +/** + * The Kt interface extends J to represent Kotlin 2 language constructs + * in OpenRewrite's LST model. This leverages the K2 compiler's FIR + * representation for enhanced type information and performance. + */ +public interface Kt extends J { + + @SuppressWarnings("unchecked") + @Override + default R accept(TreeVisitor v, P p) { + return (R) acceptKotlin2(v.adapt(Kotlin2Visitor.class), p); + } + + @Override + default

boolean isAcceptable(TreeVisitor v, P p) { + return v.isAdaptableTo(Kotlin2Visitor.class); + } + + J2 acceptKotlin2(Kotlin2Visitor

v, P p); + + @EqualsAndHashCode(callSuper = false, onlyExplicitlyIncluded = true) + @Value + class CompilationUnit implements Kt, JavaSourceFile, SourceFile { + @EqualsAndHashCode.Include + UUID id; + + Space prefix; + Markers markers; + Path sourcePath; + + @Nullable + FileMode fileMode; + + @Nullable + Charset charset; + boolean charsetBomMarked; + + @Nullable + Checksum checksum; + + @Nullable + J.Package packageDeclaration; + + List imports; + List statements; + Space eof; + + @Override + public CompilationUnit withId(UUID id) { + return this.id == id ? this : new CompilationUnit(id, prefix, markers, sourcePath, fileMode, charset, charsetBomMarked, checksum, packageDeclaration, imports, statements, eof); + } + + @Override + public CompilationUnit withPrefix(Space prefix) { + return this.prefix == prefix ? this : new CompilationUnit(id, prefix, markers, sourcePath, fileMode, charset, charsetBomMarked, checksum, packageDeclaration, imports, statements, eof); + } + + @Override + public CompilationUnit withMarkers(Markers markers) { + return this.markers == markers ? this : new CompilationUnit(id, prefix, markers, sourcePath, fileMode, charset, charsetBomMarked, checksum, packageDeclaration, imports, statements, eof); + } + + @Override + public CompilationUnit withSourcePath(Path sourcePath) { + return this.sourcePath == sourcePath ? this : new CompilationUnit(id, prefix, markers, sourcePath, fileMode, charset, charsetBomMarked, checksum, packageDeclaration, imports, statements, eof); + } + + @Override + public CompilationUnit withFileMode(@Nullable FileMode fileMode) { + return this.fileMode == fileMode ? this : new CompilationUnit(id, prefix, markers, sourcePath, fileMode, charset, charsetBomMarked, checksum, packageDeclaration, imports, statements, eof); + } + + @Override + public CompilationUnit withCharset(Charset charset) { + return this.charset == charset ? this : new CompilationUnit(id, prefix, markers, sourcePath, fileMode, charset, charsetBomMarked, checksum, packageDeclaration, imports, statements, eof); + } + + @Override + public CompilationUnit withCharsetBomMarked(boolean charsetBomMarked) { + return this.charsetBomMarked == charsetBomMarked ? this : new CompilationUnit(id, prefix, markers, sourcePath, fileMode, charset, charsetBomMarked, checksum, packageDeclaration, imports, statements, eof); + } + + @Override + public CompilationUnit withChecksum(@Nullable Checksum checksum) { + return this.checksum == checksum ? this : new CompilationUnit(id, prefix, markers, sourcePath, fileMode, charset, charsetBomMarked, checksum, packageDeclaration, imports, statements, eof); + } + + @Override + public CompilationUnit withPackageDeclaration(@Nullable J.Package packageDeclaration) { + return this.packageDeclaration == packageDeclaration ? this : new CompilationUnit(id, prefix, markers, sourcePath, fileMode, charset, charsetBomMarked, checksum, packageDeclaration, imports, statements, eof); + } + + @Override + public CompilationUnit withImports(List imports) { + return this.imports == imports ? this : new CompilationUnit(id, prefix, markers, sourcePath, fileMode, charset, charsetBomMarked, checksum, packageDeclaration, imports, statements, eof); + } + + public CompilationUnit withStatements(List statements) { + return this.statements == statements ? this : new CompilationUnit(id, prefix, markers, sourcePath, fileMode, charset, charsetBomMarked, checksum, packageDeclaration, imports, statements, eof); + } + + @Override + public CompilationUnit withEof(Space eof) { + return this.eof == eof ? this : new CompilationUnit(id, prefix, markers, sourcePath, fileMode, charset, charsetBomMarked, checksum, packageDeclaration, imports, statements, eof); + } + + @Override + public J2 acceptKotlin2(Kotlin2Visitor

v, P p) { + return (J2) v.visitCompilationUnit(this, p); + } + + @Override + public List getClasses() { + return ListUtils.map(statements, s -> s instanceof J.ClassDeclaration ? (J.ClassDeclaration) s : null); + } + + @Override + public CompilationUnit withClasses(List classes) { + return withStatements(ListUtils.map(statements, s -> { + if (s instanceof J.ClassDeclaration) { + for (J.ClassDeclaration c : classes) { + if (c.getName().getSimpleName().equals(((J.ClassDeclaration) s).getName().getSimpleName())) { + return c; + } + } + return null; + } + return s; + })); + } + + @Override + public JavaType getType() { + return null; + } + + @Override + public J2 withType(@Nullable JavaType type) { + return (J2) this; + } + + @Override + @Override + public

TreeVisitor> printer(Cursor cursor) { + return new Kotlin2Printer<>(); + } + } + + /** + * Represents a context receiver in Kotlin 2. + * Context receivers are a new feature that allows functions to declare + * required context for their execution. + */ + @EqualsAndHashCode(callSuper = false, onlyExplicitlyIncluded = true) + @Value + class ContextReceiver implements Kt { + @EqualsAndHashCode.Include + UUID id; + + Space prefix; + Markers markers; + J.TypeTree context; + + @Override + public ContextReceiver withId(UUID id) { + return this.id == id ? this : new ContextReceiver(id, prefix, markers, context); + } + + @Override + public ContextReceiver withPrefix(Space prefix) { + return this.prefix == prefix ? this : new ContextReceiver(id, prefix, markers, context); + } + + @Override + public ContextReceiver withMarkers(Markers markers) { + return this.markers == markers ? this : new ContextReceiver(id, prefix, markers, context); + } + + public ContextReceiver withContext(J.TypeTree context) { + return this.context == context ? this : new ContextReceiver(id, prefix, markers, context); + } + + @Override + public J2 acceptKotlin2(Kotlin2Visitor

v, P p) { + return (J2) v.visitContextReceiver(this, p); + } + + @Override + public JavaType getType() { + return context.getType(); + } + + @Override + public J2 withType(@Nullable JavaType type) { + return (J2) withContext(context.withType(type)); + } + } + + /** + * Represents a definitely non-nullable type in Kotlin 2. + * This is a new type system feature that guarantees a type cannot be null. + */ + @EqualsAndHashCode(callSuper = false, onlyExplicitlyIncluded = true) + @Value + class DefinitelyNonNullableType implements Kt { + @EqualsAndHashCode.Include + UUID id; + + Space prefix; + Markers markers; + J.TypeTree baseType; + + @Override + public DefinitelyNonNullableType withId(UUID id) { + return this.id == id ? this : new DefinitelyNonNullableType(id, prefix, markers, baseType); + } + + @Override + public DefinitelyNonNullableType withPrefix(Space prefix) { + return this.prefix == prefix ? this : new DefinitelyNonNullableType(id, prefix, markers, baseType); + } + + @Override + public DefinitelyNonNullableType withMarkers(Markers markers) { + return this.markers == markers ? this : new DefinitelyNonNullableType(id, prefix, markers, baseType); + } + + public DefinitelyNonNullableType withBaseType(J.TypeTree baseType) { + return this.baseType == baseType ? this : new DefinitelyNonNullableType(id, prefix, markers, baseType); + } + + @Override + public J2 acceptKotlin2(Kotlin2Visitor

v, P p) { + return (J2) v.visitDefinitelyNonNullableType(this, p); + } + + @Override + public JavaType getType() { + return baseType.getType(); + } + + @Override + public J2 withType(@Nullable JavaType type) { + return (J2) withBaseType(baseType.withType(type)); + } + } +} \ No newline at end of file diff --git a/rewrite-kotlin2/src/main/kotlin/org/openrewrite/kotlin2/Kotlin2TypeMapping.kt b/rewrite-kotlin2/src/main/kotlin/org/openrewrite/kotlin2/Kotlin2TypeMapping.kt new file mode 100644 index 0000000000..76513f10e7 --- /dev/null +++ b/rewrite-kotlin2/src/main/kotlin/org/openrewrite/kotlin2/Kotlin2TypeMapping.kt @@ -0,0 +1,272 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.kotlin2 + +import org.jetbrains.kotlin.KtFakeSourceElementKind +import org.jetbrains.kotlin.builtins.PrimitiveType +import org.jetbrains.kotlin.codegen.classId +import org.jetbrains.kotlin.codegen.topLevelClassAsmType +import org.jetbrains.kotlin.descriptors.ClassKind +import org.jetbrains.kotlin.descriptors.Modality +import org.jetbrains.kotlin.descriptors.Visibility +import org.jetbrains.kotlin.fir.* +import org.jetbrains.kotlin.fir.FirImplementationDetail +import org.jetbrains.kotlin.fir.analysis.checkers.getContainingClassSymbol +import org.jetbrains.kotlin.fir.analysis.checkers.modality +import org.jetbrains.kotlin.fir.analysis.checkers.toRegularClassSymbol +import org.jetbrains.kotlin.fir.declarations.* +import org.jetbrains.kotlin.fir.declarations.impl.FirPrimaryConstructor +import org.jetbrains.kotlin.fir.declarations.utils.isLocal +import org.jetbrains.kotlin.fir.declarations.utils.isStatic +import org.jetbrains.kotlin.fir.declarations.utils.modality +import org.jetbrains.kotlin.fir.declarations.utils.visibility +import org.jetbrains.kotlin.fir.expressions.* +import org.jetbrains.kotlin.fir.java.declarations.FirJavaField +import org.jetbrains.kotlin.fir.references.FirErrorNamedReference +import org.jetbrains.kotlin.fir.references.FirResolvedNamedReference +import org.jetbrains.kotlin.fir.references.FirSuperReference +import org.jetbrains.kotlin.fir.references.toResolvedBaseSymbol +import org.jetbrains.kotlin.fir.resolve.calls.FirSyntheticFunctionSymbol +import org.jetbrains.kotlin.fir.resolve.providers.toSymbol +import org.jetbrains.kotlin.fir.resolve.toFirRegularClass +import org.jetbrains.kotlin.fir.resolve.toSymbol +import org.jetbrains.kotlin.fir.symbols.SymbolInternals +import org.jetbrains.kotlin.fir.symbols.impl.* +import org.jetbrains.kotlin.fir.types.* +import org.jetbrains.kotlin.fir.types.impl.FirImplicitNullableAnyTypeRef +import org.jetbrains.kotlin.fir.types.jvm.FirJavaTypeRef +import org.jetbrains.kotlin.load.java.structure.* +import org.jetbrains.kotlin.load.java.structure.impl.classFiles.* +import org.jetbrains.kotlin.load.kotlin.JvmPackagePartSource +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.isOneSegmentFQN +import org.jetbrains.kotlin.resolve.jvm.JvmClassName +import org.jetbrains.kotlin.types.ConstantValueKind +import org.jetbrains.kotlin.types.Variance +import org.openrewrite.java.JavaTypeMapping +import org.openrewrite.java.internal.JavaTypeCache +import org.openrewrite.java.tree.JavaType +import org.openrewrite.java.tree.JavaType.* +import org.openrewrite.kotlin2.Kotlin2TypeSignatureBuilder.Companion.convertClassIdToFqn +import org.openrewrite.kotlin2.Kotlin2TypeSignatureBuilder.Companion.methodName +import org.openrewrite.kotlin2.Kotlin2TypeSignatureBuilder.Companion.variableName +import kotlin.collections.ArrayList + +/** + * Type mapping for Kotlin 2 using the K2 compiler's FIR representation. + * This class maps FIR types to OpenRewrite's JavaType system, leveraging + * the enhanced type information available in the K2 compiler. + */ +@Suppress("DuplicatedCode") +class Kotlin2TypeMapping( + private val typeCache: JavaTypeCache, + val firSession: FirSession, + private val firFile: FirFile +) : JavaTypeMapping { + + private val signatureBuilder: Kotlin2TypeSignatureBuilder = Kotlin2TypeSignatureBuilder(firSession, firFile) + + override fun type(type: Any?): JavaType { + if (type == null || type is FirErrorTypeRef || (type is FirResolvedQualifier && type.classId == null)) { + return Unknown.getInstance() + } + + val signature = signatureBuilder.signature(type) + val existing: JavaType? = typeCache.get(signature) + if (existing != null) { + return existing + } + + return type(type, firFile, signature) ?: Unknown.getInstance() + } + + fun type(type: Any?, parent: Any?): JavaType? { + if (type == null || type is FirErrorTypeRef || (type is FirResolvedQualifier && type.classId == null)) { + return Unknown.getInstance() + } + val signature = signatureBuilder.signature(type, parent) + val existing = typeCache.get(signature) + if (existing != null) { + return existing + } + return type(type, parent, signature) + } + + private fun type(type: Any, parent: Any?, signature: String): JavaType? { + // Implementation for K2 compiler FIR type mapping + // This will handle the new FIR types and enhanced type information + // available in Kotlin 2.0 + + return when (type) { + is FirTypeRef -> mapFirTypeRef(type, signature) + is FirClass -> mapFirClass(type, signature) + is FirFunction -> mapFirFunction(type, signature) + is FirProperty -> mapFirProperty(type, signature) + is FirExpression -> mapFirExpression(type, signature) + is FirResolvedQualifier -> mapFirResolvedQualifier(type, signature) + else -> null + } + } + + private fun mapFirTypeRef(typeRef: FirTypeRef, signature: String): JavaType? { + // Map FIR type references to JavaType + return when (typeRef) { + is FirResolvedTypeRef -> mapConeType(typeRef.type, signature) + is FirImplicitNullableAnyTypeRef -> typeCache.get("java.lang.Object") + is FirJavaTypeRef -> mapJavaTypeRef(typeRef, signature) + else -> null + } + } + + private fun mapConeType(coneType: ConeKotlinType, signature: String): JavaType? { + // Map Cone types (K2's internal type representation) to JavaType + return when (coneType) { + is ConeClassLikeType -> mapClassLikeType(coneType, signature) + is ConeTypeParameterType -> mapTypeParameter(coneType, signature) + is ConeFlexibleType -> mapFlexibleType(coneType, signature) + is ConeIntersectionType -> mapIntersectionType(coneType, signature) + is ConeDefinitelyNotNullType -> mapDefinitelyNotNullType(coneType, signature) + else -> null + } + } + + private fun mapClassLikeType(type: ConeClassLikeType, signature: String): JavaType? { + // Map class-like types to JavaType.Class + val classId = type.classId ?: return null + val fqn = convertClassIdToFqn(classId) + + // Check cache first + val cached = typeCache.get(signature) + if (cached != null) { + return cached + } + + // Create new Class type + val classType = Class( + null, + 0L, // flagsBitMap + fqn, + JavaType.FullyQualified.Kind.Class, + emptyList(), + null, + null, + emptyList(), + emptyList(), + emptyList(), + emptyList() + ) + + typeCache.put(signature, classType) + return classType + } + + private fun mapTypeParameter(type: ConeTypeParameterType, signature: String): JavaType? { + // Map type parameters to JavaType.GenericTypeVariable + val name = type.lookupTag.name.asString() + return GenericTypeVariable(null, name, GenericTypeVariable.Variance.INVARIANT, emptyList()) + } + + private fun mapFlexibleType(type: ConeFlexibleType, signature: String): JavaType? { + // K2 compiler uses flexible types for platform types + // Map to the upper bound for safety + return mapConeType(type.upperBound, signature) + } + + private fun mapIntersectionType(type: ConeIntersectionType, signature: String): JavaType? { + // Map intersection types + val types = type.intersectedTypes.mapNotNull { mapConeType(it, "${signature}_${it}") } + return if (types.isNotEmpty()) types.first() else null + } + + private fun mapDefinitelyNotNullType(type: ConeDefinitelyNotNullType, signature: String): JavaType? { + // K2 specific: definitely non-nullable types + // Map to the original type but mark as non-nullable + return mapConeType(type.original, signature) + } + + private fun mapJavaTypeRef(typeRef: FirJavaTypeRef, signature: String): JavaType? { + // Map Java type references + return null // TODO: Implement Java type mapping + } + + private fun mapFirClass(firClass: FirClass, signature: String): JavaType? { + // Map FIR class declarations to JavaType.Class + val classId = firClass.symbol.classId + val fqn = convertClassIdToFqn(classId) + + return Class( + null, + 0L, // flagsBitMap + fqn, + when (firClass.classKind) { + ClassKind.CLASS -> JavaType.FullyQualified.Kind.Class + ClassKind.INTERFACE -> JavaType.FullyQualified.Kind.Interface + ClassKind.ENUM_CLASS -> JavaType.FullyQualified.Kind.Enum + ClassKind.ANNOTATION_CLASS -> JavaType.FullyQualified.Kind.Annotation + else -> JavaType.FullyQualified.Kind.Class + }, + emptyList(), + null, + null, + emptyList(), + emptyList(), + emptyList(), + emptyList() + ) + } + + private fun mapFirFunction(function: FirFunction, signature: String): JavaType? { + // Map FIR functions to JavaType.Method + return null // TODO: Implement function mapping + } + + private fun mapFirProperty(property: FirProperty, signature: String): JavaType? { + // Map FIR properties to JavaType.Variable + return null // TODO: Implement property mapping + } + + private fun mapFirExpression(expression: FirExpression, signature: String): JavaType? { + // Map FIR expressions based on their resolved type + // Using resolvedType which is the proper API for resolved expressions + return try { + val coneType = expression.resolvedType + mapConeType(coneType, signature) + } catch (e: Exception) { + // Expression type not yet resolved + null + } + } + + private fun mapFirResolvedQualifier(qualifier: FirResolvedQualifier, signature: String): JavaType? { + // Map resolved qualifiers to their class types + val classId = qualifier.classId ?: return null + val fqn = convertClassIdToFqn(classId) + + return Class( + null, + 0L, // flagsBitMap + fqn, + JavaType.FullyQualified.Kind.Class, + emptyList(), + null, + null, + emptyList(), + emptyList(), + emptyList(), + emptyList() + ) + } +} \ No newline at end of file diff --git a/rewrite-kotlin2/src/main/kotlin/org/openrewrite/kotlin2/Kotlin2TypeSignatureBuilder.kt b/rewrite-kotlin2/src/main/kotlin/org/openrewrite/kotlin2/Kotlin2TypeSignatureBuilder.kt new file mode 100644 index 0000000000..834d929253 --- /dev/null +++ b/rewrite-kotlin2/src/main/kotlin/org/openrewrite/kotlin2/Kotlin2TypeSignatureBuilder.kt @@ -0,0 +1,381 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.kotlin2 + +import org.jetbrains.kotlin.builtins.PrimitiveType +import org.jetbrains.kotlin.fir.* +import org.jetbrains.kotlin.fir.FirImplementationDetail +import org.jetbrains.kotlin.fir.declarations.* +import org.jetbrains.kotlin.fir.declarations.utils.classId +import org.jetbrains.kotlin.fir.expressions.* +import org.jetbrains.kotlin.fir.references.FirErrorNamedReference +import org.jetbrains.kotlin.fir.references.FirResolvedNamedReference +import org.jetbrains.kotlin.fir.references.FirSuperReference +import org.jetbrains.kotlin.fir.references.toResolvedBaseSymbol +import org.jetbrains.kotlin.fir.resolve.calls.FirSyntheticFunctionSymbol +import org.jetbrains.kotlin.fir.resolve.inference.ConeTypeParameterBasedTypeVariable +import org.jetbrains.kotlin.fir.resolve.providers.toSymbol +import org.jetbrains.kotlin.fir.resolve.toFirRegularClass +import org.jetbrains.kotlin.fir.symbols.SymbolInternals +import org.jetbrains.kotlin.fir.symbols.impl.* +import org.jetbrains.kotlin.fir.types.* +import org.jetbrains.kotlin.fir.types.impl.FirImplicitNullableAnyTypeRef +import org.jetbrains.kotlin.fir.types.jvm.FirJavaTypeRef +import org.jetbrains.kotlin.load.java.structure.* +import org.jetbrains.kotlin.load.java.structure.impl.classFiles.BinaryJavaAnnotation +import org.jetbrains.kotlin.load.java.structure.impl.classFiles.BinaryJavaClass +import org.jetbrains.kotlin.load.java.structure.impl.classFiles.BinaryJavaTypeParameter +import org.jetbrains.kotlin.load.kotlin.JvmPackagePartSource +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.resolve.jvm.JvmClassName +import org.jetbrains.kotlin.types.Variance +import org.openrewrite.java.JavaTypeSignatureBuilder +import org.openrewrite.java.tree.JavaType +import java.util.* +import kotlin.collections.HashMap + +/** + * Type signature builder for Kotlin 2 using the K2 compiler's FIR representation. + * This class generates unique signatures for FIR types to enable caching and comparison. + */ +@Suppress("DuplicatedCode") +class Kotlin2TypeSignatureBuilder(private val firSession: FirSession, private val firFile: FirFile) : + JavaTypeSignatureBuilder { + private var typeVariableNameStack: MutableSet? = null + + override fun signature(type: Any?): String { + return signature(type, firFile) + } + + override fun classSignature(type: Any): String { + return when (type) { + is ConeClassLikeType -> classSignature(type) + is FirClass -> classSignature(type) + else -> "{undefined}" + } + } + + override fun parameterizedSignature(type: Any): String { + return when (type) { + is ConeClassLikeType -> parameterizedSignature(type) + is FirClass -> parameterizedSignature(type) + else -> "{undefined}" + } + } + + override fun arraySignature(type: Any): String { + // TODO: Implement array signature for Kotlin arrays + return "{array}" + } + + override fun genericSignature(type: Any): String { + return when (type) { + is FirTypeParameter -> typeVariableSignature(type) + else -> "{generic}" + } + } + + override fun primitiveSignature(type: Any): String { + // Handle Kotlin primitive types + return when (type) { + is ConeClassLikeType -> { + val classId = type.classId + when (classId?.asFqNameString()) { + "kotlin.Boolean" -> "boolean" + "kotlin.Byte" -> "byte" + "kotlin.Char" -> "char" + "kotlin.Short" -> "short" + "kotlin.Int" -> "int" + "kotlin.Long" -> "long" + "kotlin.Float" -> "float" + "kotlin.Double" -> "double" + "kotlin.Unit" -> "void" + else -> "{primitive}" + } + } + else -> "{primitive}" + } + } + + @OptIn(SymbolInternals::class) + fun signature(type: Any?, parent: Any?): String { + return when (type) { + is ConeClassLikeType -> { + if (type.typeArguments.isNotEmpty()) parameterizedSignature(type) else classSignature(type) + } + + is ConeFlexibleType -> { + signature(type.lowerBound) + } + + is ConeStubTypeForChainInference -> { + signature(type.constructor.variable) + } + + is ConeTypeProjection -> { + coneTypeProjectionSignature(type) + } + + is ConeTypeParameterBasedTypeVariable -> { + signature(type.typeParameterSymbol.fir) + } + + is ConeDefinitelyNotNullType -> { + // K2 specific: definitely non-nullable types + "!" + signature(type.original) + } + + is ConeIntersectionType -> { + // K2 specific: intersection types + type.intersectedTypes.joinToString("&") { signature(it) } + } + + is FirAnonymousFunctionExpression -> { + signature(type.anonymousFunction) + } + + is FirBlock -> { + "{undefined}" + } + + is FirAnonymousObject -> { + if (type.typeParameters.isNotEmpty()) anonymousParameterizedSignature(type) else anonymousClassSignature(type) + } + + is FirClass -> { + if (type.typeParameters.isNotEmpty()) parameterizedSignature(type) else classSignature(type) + } + + is FirConstructor -> { + constructorSignature(type) + } + + is FirEnumEntry -> { + signature(type.returnTypeRef, parent) + } + + is FirErrorNamedReference -> { + "{undefined}" + } + + is FirExpression -> { + // Use resolvedType for FIR expressions + try { + signature(type.resolvedType, parent) + } catch (e: Exception) { + "{undefined}" + } + } + + is FirFile -> { + convertClassIdToFqn(type.packageFqName.asString()) + } + + is FirFunction -> { + methodSignature(type, parent) + } + + is FirImplicitNullableAnyTypeRef -> { + "java.lang.Object" + } + + is FirJavaTypeRef -> { + // TODO: Implement Java type signature generation + "{java-type}" + } + + is FirProperty -> { + variableSignature(type, parent) + } + + is FirResolvedNamedReference -> { + signature(type.toResolvedBaseSymbol()?.fir, parent) + } + + is FirResolvedQualifier -> { + signature(type.symbol?.fir, parent) + } + + is FirResolvedTypeRef -> { + signature(type.type, parent) + } + + is FirSuperReference -> { + "{super}" + } + + is FirTypeAlias -> { + signature(type.expandedTypeRef, parent) + } + + is FirTypeParameter -> { + typeVariableSignature(type) + } + + is FirTypeProjectionWithVariance -> { + signature(type.typeRef, parent) + } + + is FirTypeRef -> { + "{undefined}" + } + + is FirVariable -> { + variableSignature(type, parent) + } + + else -> { + "{undefined}" + } + } + } + + private fun classSignature(type: ConeClassLikeType): String { + return convertClassIdToFqn(type.classId!!) + } + + private fun classSignature(type: FirClass): String { + return convertClassIdToFqn(type.classId) + } + + private fun parameterizedSignature(type: ConeClassLikeType): String { + val baseType = classSignature(type) + val params = type.typeArguments.joinToString(",", "<", ">") { + when (it) { + is ConeStarProjection -> "*" + is ConeKotlinTypeProjection -> signature(it.type) + else -> "{undefined}" + } + } + return baseType + params + } + + private fun parameterizedSignature(type: FirClass): String { + val baseType = classSignature(type) + val params = type.typeParameters.joinToString(",", "<", ">") { + signature(it) + } + return baseType + params + } + + private fun anonymousClassSignature(type: FirAnonymousObject): String { + return "Anonymous{" + type.symbol.toString() + "}" + } + + private fun anonymousParameterizedSignature(type: FirAnonymousObject): String { + val baseType = anonymousClassSignature(type) + val params = type.typeParameters.joinToString(",", "<", ">") { + signature(it) + } + return baseType + params + } + + private fun coneTypeProjectionSignature(type: ConeTypeProjection): String { + return when (type) { + is ConeStarProjection -> "*" + is ConeKotlinTypeProjection -> { + val variance = when (type.kind) { + ProjectionKind.IN -> "in " + ProjectionKind.OUT -> "out " + ProjectionKind.INVARIANT -> "" + ProjectionKind.STAR -> "*" + } + variance + signature(type.type) + } + } + } + + private fun constructorSignature(constructor: FirConstructor): String { + val declaringType = constructor.symbol.containingClassLookupTag()?.classId + val className = if (declaringType != null) convertClassIdToFqn(declaringType) else "{undefined}" + val params = constructor.valueParameters.joinToString(",", "(", ")") { + signature(it.returnTypeRef) + } + return "$className$params" + } + + private fun methodSignature(function: FirFunction, parent: Any?): String { + val name = methodName(function) + val declaringType = when (parent) { + is FirClass -> convertClassIdToFqn(parent.classId) + is FirFile -> convertClassIdToFqn(parent.packageFqName.asString()) + else -> "{undefined}" + } + val params = function.valueParameters.joinToString(",", "(", ")") { + signature(it.returnTypeRef) + } + return "$declaringType.$name$params" + } + + private fun variableSignature(variable: FirVariable, parent: Any?): String { + val name = variableName(variable) + val declaringType = when (parent) { + is FirClass -> convertClassIdToFqn(parent.classId) + is FirFile -> convertClassIdToFqn(parent.packageFqName.asString()) + else -> "{undefined}" + } + return "$declaringType.$name" + } + + private fun typeVariableSignature(typeParameter: FirTypeParameter): String { + val name = typeParameter.name.asString() + if (typeVariableNameStack?.contains(name) == true) { + return "Generic{$name}" + } + + if (typeVariableNameStack == null) { + typeVariableNameStack = HashSet() + } + typeVariableNameStack!!.add(name) + + val bounds = typeParameter.bounds.joinToString("&") { + signature(it) + } + + typeVariableNameStack!!.remove(name) + + return if (bounds.isNotEmpty()) { + "Generic{$name extends $bounds}" + } else { + "Generic{$name}" + } + } + + + companion object { + fun convertClassIdToFqn(classId: ClassId): String { + return classId.asFqNameString().replace('/', '.') + } + + fun convertClassIdToFqn(packageName: String): String { + return packageName.replace('/', '.') + } + + fun methodName(function: FirFunction): String { + return when (function) { + is FirConstructor -> "" + is FirSimpleFunction -> function.name.asString() + is FirAnonymousFunction -> "" + else -> "{undefined}" + } + } + + fun variableName(variable: FirVariable): String { + return variable.name.asString() + } + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 6d8f74fe0d..0dd350055b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -29,6 +29,7 @@ val allProjects = listOf( "rewrite-javascript", "rewrite-json", "rewrite-kotlin", + "rewrite-kotlin2", "rewrite-maven", "rewrite-properties", "rewrite-protobuf",