Skip to content

feat(domain): add structured container and import/export semantics for cross-language clarity#42

Merged
phodal merged 5 commits intomasterfrom
feature/structured-container-semantics
Jan 18, 2026
Merged

feat(domain): add structured container and import/export semantics for cross-language clarity#42
phodal merged 5 commits intomasterfrom
feature/structured-container-semantics

Conversation

@phodal
Copy link
Owner

@phodal phodal commented Jan 17, 2026

Summary

This PR addresses Issue #41 - solving Problem 1 and Problem 3:

  1. PackageName / Module / Namespace semantic mixing across languages
  2. CodeImport/CodeExport being too string-based

Problem 1: Container Semantics

The PackageName field in CodeContainer had different meanings across languages:

  • Java/Kotlin/Go: package declaration
  • C#/C++: namespace
  • Rust: module path derived from file path (crate/mod)
  • TypeScript: resolved module path
  • Toml: table name
  • Protobuf/Thrift: IDL package

Solution

Add structured fields to CodeContainer while maintaining backward compatibility:

  1. ContainerKind enum - distinguishes semantic container types:

    • SOURCE_FILE, PACKAGE, NAMESPACE, MODULE, CRATE, CONFIG, BUILD_SCRIPT, IDL, UNKNOWN
  2. New fields:

    • Language: String - the programming language
    • Kind: ContainerKind - semantic container type
    • DeclaredPackage: String - explicitly declared package/namespace
    • ResolvedModulePath: String - path-derived module name
    • NamespacePath: List<String> - namespace hierarchy as list

Problem 3: Import/Export Semantics

The original CodeImport with UsageName: List<String> + AsName: String couldn't accurately express:

  • TypeScript: default/named/namespace/type-only imports, re-exports
  • Rust: path trees like use a::{b, c as d}
  • Python: relative imports, wildcard imports
  • Java: static imports
  • Scala: import selectors with rename
  • Go: dot imports, blank imports

Solution

CodeImport new fields:

  • ImportKind enum: UNKNOWN, DEFAULT, NAMED, NAMESPACE, SIDE_EFFECT, STATIC, WILDCARD, RELATIVE, TYPE_ONLY, DOT
  • ImportSpecifier data class: OriginalName, LocalName, IsTypeOnly
  • New fields: Kind, Specifiers, DefaultName, NamespaceName, IsTypeOnly, PathSegments

CodeExport new fields:

  • ExportKind enum: UNKNOWN, DEFAULT, NAMED, RE_EXPORT_ALL, RE_EXPORT_NAMED, TYPE_ONLY
  • ExportSpecifier data class: LocalName, ExportedName, IsTypeOnly
  • New fields: Kind, Specifiers, FromSource, IsTypeOnly

Examples

// TypeScript: import { foo, bar as baz } from "module"
CodeImport(
    Source = "module",
    Kind = ImportKind.NAMED,
    Specifiers = listOf(
        ImportSpecifier(OriginalName = "foo", LocalName = "foo"),
        ImportSpecifier(OriginalName = "bar", LocalName = "baz")
    )
)

// TypeScript: import * as utils from "./utils"
CodeImport(
    Source = "./utils",
    Kind = ImportKind.NAMESPACE,
    NamespaceName = "utils"
)

// Java: import static org.junit.Assert.assertEquals
CodeImport(
    Source = "org.junit.Assert",
    Kind = ImportKind.STATIC,
    Scope = "static",
    Specifiers = listOf(ImportSpecifier(OriginalName = "assertEquals", LocalName = "assertEquals"))
)

// TypeScript: export { foo as bar } from "module"
CodeExport(
    Name = "bar",
    Kind = ExportKind.RE_EXPORT_NAMED,
    FromSource = "module",
    Specifiers = listOf(ExportSpecifier(LocalName = "foo", ExportedName = "bar"))
)

Backward Compatibility

  • All legacy fields are preserved and still populated:
    • CodeContainer.PackageName
    • CodeImport.Source, CodeImport.AsName, CodeImport.UsageName
    • CodeExport.Name, CodeExport.SourceFile
  • New fields have sensible defaults
  • Existing downstream code continues to work unchanged

Test plan

  • All existing parser tests pass
  • New unit tests for CodeImport covering all language patterns
  • New unit tests for CodeContainer covering all container kinds
  • Full project build successful

Partially addresses #41 (problems 1 and 3 of 5)

Summary by CodeRabbit

  • New Features

    • Cross-language container metadata (Language, Kind) plus DeclaredPackage, ResolvedModulePath, NamespacePath.
    • Structured import and export models: import kinds, specifiers, path segments; export kinds and specifiers.
    • Publishing enhancements: Central Portal endpoints, credential resolution order, GPG agent support, and longer timeouts.
  • Tests

    • Extensive unit tests for CodeContainer and CodeImport (multi-language) and backward compatibility.

✏️ Tip: You can customize this high-level summary in your review settings.

Add helper to read Maven settings.xml for Sonatype credentials and configure Nexus publishing with new Central Portal endpoints. Also ensure kotlinSourcesJar depends on ANTLR tasks.
Replace direct key/password configuration with GPG agent for more secure signing. Fallback to traditional method if GPG agent is not configured.
…larity

This commit addresses the semantic mixing issue where `PackageName` had
different meanings across languages (package/namespace/module/config table).

Changes:
- Add `ContainerKind` enum to distinguish container types:
  - SOURCE_FILE, PACKAGE, NAMESPACE, MODULE, CRATE, CONFIG, BUILD_SCRIPT, IDL
- Add new structured fields to `CodeContainer`:
  - `Language`: the programming language (java, typescript, rust, etc.)
  - `Kind`: semantic container type
  - `DeclaredPackage`: explicitly declared package/namespace in source
  - `ResolvedModulePath`: path-derived module name (for Rust, Python, TS)
  - `NamespacePath`: namespace hierarchy as list (for C#, C++, TS)
- Update all language parsers to fill the new fields:
  - Java/Kotlin/Go/Scala: DeclaredPackage from package declaration
  - Rust/Python/TypeScript: ResolvedModulePath from file path
  - C#/C++/TypeScript: NamespacePath for namespace hierarchies
  - Toml: Kind=CONFIG
  - CMake: Kind=BUILD_SCRIPT
  - Protobuf/Thrift: Kind=IDL

Backward compatibility:
- Legacy `PackageName` field is preserved and still populated
- New fields have sensible defaults (empty strings, UNKNOWN kind)

Closes #41
Copilot AI review requested due to automatic review settings January 17, 2026 15:29
@coderabbitai
Copy link

coderabbitai bot commented Jan 17, 2026

Caution

Review failed

The pull request is closed.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

This PR extends the core domain model with structured container, import, and export metadata, updates many language listeners to populate those fields, and revises build/publishing to use OSSRH Central Portal endpoints with Maven settings credential parsing, GPG-agent signing support, and longer Nexus timeouts.

Changes

Cohort / File(s) Summary
Core domain model
chapi-domain/src/main/kotlin/chapi/domain/core/CodeContainer.kt, chapi-domain/src/main/kotlin/chapi/domain/core/CodeImport.kt, chapi-domain/src/main/kotlin/chapi/domain/core/CodeExport.kt
Added ContainerKind enum; expanded CodeContainer with Language, Kind, DeclaredPackage, ResolvedModulePath, NamespacePath; introduced ImportKind, ImportSpecifier, structured CodeImport; introduced ExportKind, ExportSpecifier, structured CodeExport. Legacy fields retained.
Domain tests
chapi-domain/src/test/kotlin/chapi/domain/core/CodeContainerTest.kt, chapi-domain/src/test/kotlin/chapi/domain/core/CodeImportTest.kt
Added/expanded unit tests validating new container/import/export semantics across languages and preserving legacy behaviors.
Language listeners — container & imports/exports
chapi-ast-java/.../JavaBasicIdentListener.kt, .../JavaFullIdentListener.kt, chapi-ast-kotlin/.../KotlinBasicIdentListener.kt, chapi-ast-scala/.../ScalaFullIdentListener.kt, chapi-ast-c/.../CFullIdentListener.kt, chapi-ast-cpp/.../CPPBasicIdentListener.kt, chapi-ast-csharp/.../CSharpAstListener.kt, chapi-ast-go/.../GoFullIdentListener.kt, chapi-ast-python/.../PythonFullIdentListener.kt, chapi-ast-rust/.../RustAstBaseListener.kt, chapi-ast-protobuf/.../Protobuf*FullIdentListener.kt, chapi-ast-thrift/.../ThriftFullIdentListener.kt, chapi-ast-typescript/.../TypeScriptFullIdentListener.kt
Updated CodeContainer initialization to include Language and appropriate Kind. Many listeners now set DeclaredPackage, ResolvedModulePath, NamespacePath and populate structured CodeImport/Specifiers/PathSegments/Kind and enriched export handling (notably TypeScript, Java, Python, Rust, Go, Scala).
Parsers for config/build
chapi-parser-cmake/.../CMakeBasicListener.kt, chapi-parser-toml/.../TomlListener.kt
Root and table containers now include Language ("cmake"/"toml") and Kind (BUILD_SCRIPT/CONFIG); Toml table containers created with structured metadata.
Build and publishing
build.gradle.kts
Switched Sonatype endpoints to Central Portal; added getMavenCredentials() to parse Maven settings.xml; exposed mavenUsername/mavenPassword; prefer gradle props → env vars → Maven settings for Nexus credentials; added signing.gnupg.keyName GPG-agent path with fallback to legacy signing props; increased Nexus connectTimeout and clientTimeout to 3 minutes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I nibble bytes and stitch each name,
Tags and kinds hop into frame,
Imports lined up, exports in tow,
Namespaces grow where carrots go.
Hooray for structured fields—hip-hop, let's code!

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding structured container and import/export semantics to support cross-language clarity in the chapi-domain model.
Linked Issues check ✅ Passed The PR addresses Problem 1 (container semantics) and Problem 3 (import/export structures) from Issue #41 with appropriate structural fields and multi-language parser updates.
Out of Scope Changes check ✅ Passed All changes align with Issue #41 requirements: ContainerKind/Language/DeclaredPackage fields in CodeContainer, ImportKind/Specifiers in CodeImport, ExportKind/Specifiers in CodeExport, and coordinated parser updates across 14 language modules.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces structured container semantics to improve cross-language code analysis by disambiguating the overloaded PackageName field. It adds a ContainerKind enum and five new fields (Language, Kind, DeclaredPackage, ResolvedModulePath, NamespacePath) to the CodeContainer class, enabling clearer semantic distinction between packages, namespaces, and modules across 14 languages.

Changes:

  • Added ContainerKind enum with 9 semantic container types (SOURCE_FILE, PACKAGE, NAMESPACE, MODULE, CRATE, CONFIG, BUILD_SCRIPT, IDL, UNKNOWN)
  • Extended CodeContainer with structured fields for language-specific semantics while maintaining backward compatibility
  • Updated all 14 language parsers to populate the new fields appropriately

Reviewed changes

Copilot reviewed 18 out of 19 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
chapi-domain/src/main/kotlin/chapi/domain/core/CodeContainer.kt Added ContainerKind enum and new structured fields with comprehensive documentation
chapi-domain/src/test/kotlin/chapi/domain/core/CodeContainerTest.kt Added unit tests demonstrating usage of new fields across different languages
chapi-ast-java/src/main/kotlin/chapi/ast/javaast/JavaFullIdentListener.kt Updated to set Language="java", Kind=SOURCE_FILE, and DeclaredPackage
chapi-ast-java/src/main/kotlin/chapi/ast/javaast/JavaBasicIdentListener.kt Updated to set Language="java", Kind=SOURCE_FILE, and DeclaredPackage
chapi-ast-kotlin/src/main/kotlin/chapi/ast/kotlinast/KotlinBasicIdentListener.kt Updated to set Language="kotlin", Kind=SOURCE_FILE, and DeclaredPackage
chapi-ast-go/src/main/kotlin/chapi/ast/goast/GoFullIdentListener.kt Updated to set Language="go", Kind=SOURCE_FILE, and DeclaredPackage
chapi-ast-scala/src/main/kotlin/chapi/ast/scalaast/ScalaFullIdentListener.kt Updated to set Language="scala", Kind=SOURCE_FILE, and DeclaredPackage
chapi-ast-typescript/src/main/kotlin/chapi/ast/typescriptast/TypeScriptFullIdentListener.kt Updated to set Language="typescript", Kind=MODULE, ResolvedModulePath, and NamespacePath with proper enter/exit handling
chapi-ast-rust/src/main/kotlin/chapi/ast/rustast/RustAstBaseListener.kt Updated to set Language="rust", Kind=MODULE, and ResolvedModulePath
chapi-ast-python/src/main/kotlin/chapi/ast/pythonast/PythonFullIdentListener.kt Updated to set Language="python", Kind=MODULE, and ResolvedModulePath
chapi-ast-csharp/src/main/kotlin/chapi/ast/csharpast/CSharpAstListener.kt Updated to set Language="csharp", Kind=NAMESPACE, DeclaredPackage, and NamespacePath with proper namespace stack management
chapi-ast-cpp/src/main/kotlin/chapi/ast/cppast/CPPBasicIdentListener.kt Updated to set Language="cpp", Kind=SOURCE_FILE, DeclaredPackage, and NamespacePath
chapi-ast-c/src/main/kotlin/chapi/ast/cast/CFullIdentListener.kt Updated to set Language="c", Kind=SOURCE_FILE
chapi-ast-protobuf/src/main/kotlin/chapi/ast/protobuf/ProtobufFullIdentListener.kt Updated to set Language="protobuf", Kind=IDL, and DeclaredPackage
chapi-ast-protobuf/src/main/kotlin/chapi/ast/protobuf/Protobuf2FullIdentListener.kt Updated to set Language="protobuf", Kind=IDL, and DeclaredPackage
chapi-ast-thrift/src/main/kotlin/chapi/ast/thrift/ThriftFullIdentListener.kt Updated to set Language="thrift", Kind=IDL, and DeclaredPackage
chapi-parser-toml/src/main/kotlin/chapi/parser/toml/TomlListener.kt Updated to set Language="toml", Kind=CONFIG
chapi-parser-cmake/src/main/kotlin/chapi/parser/cmake/CMakeBasicListener.kt Updated to set Language="cmake", Kind=BUILD_SCRIPT
build.gradle.kts Unrelated infrastructure changes for Sonatype Central Portal migration and Maven credentials handling

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

FullName = fileName,
Language = "python",
Kind = ContainerKind.MODULE,
ResolvedModulePath = fileName.substringBeforeLast('.').replace('/', '.').replace('\\', '.')
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Python ResolvedModulePath calculation uses fileName with path separators replaced by dots, but doesn't account for potential edge cases such as files with multiple dots in the name (e.g., "test.utils.py" would become "test.utils" which may not be the intended module path). Consider using only the directory path for module resolution, not the full filename.

Copilot uses AI. Check for mistakes.
codeContainer.NamespacePath = codeContainer.NamespacePath + nsName
}
}

Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The C++ namespace handling only appends to NamespacePath when entering a namespace but never removes from it when exiting. This will cause incorrect namespace paths for nested namespaces or multiple namespaces in the same file. Consider implementing an exit handler similar to the C# and TypeScript implementations.

Suggested change
override fun exitNamespaceDefinition(ctx: CPP14Parser.NamespaceDefinitionContext?) {
if (codeContainer.NamespacePath.isNotEmpty()) {
codeContainer.NamespacePath = codeContainer.NamespacePath.dropLast(1)
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +32 to +136
@Test
fun `should support structured container semantics for Java`() {
val javaContainer = CodeContainer(
FullName = "UserService.java",
PackageName = "com.example.service",
Language = "java",
Kind = ContainerKind.SOURCE_FILE,
DeclaredPackage = "com.example.service"
)

assertEquals("java", javaContainer.Language)
assertEquals(ContainerKind.SOURCE_FILE, javaContainer.Kind)
assertEquals("com.example.service", javaContainer.DeclaredPackage)
// Legacy field should still work
assertEquals("com.example.service", javaContainer.PackageName)
}

@Test
fun `should support structured container semantics for Rust`() {
val rustContainer = CodeContainer(
FullName = "parser.rs",
PackageName = "crate::infrastructure::parser",
Language = "rust",
Kind = ContainerKind.MODULE,
ResolvedModulePath = "crate::infrastructure::parser"
)

assertEquals("rust", rustContainer.Language)
assertEquals(ContainerKind.MODULE, rustContainer.Kind)
assertEquals("crate::infrastructure::parser", rustContainer.ResolvedModulePath)
// DeclaredPackage should be empty for Rust (no package declaration)
assertEquals("", rustContainer.DeclaredPackage)
}

@Test
fun `should support namespace path for CSharp`() {
val csharpContainer = CodeContainer(
FullName = "UserService.cs",
PackageName = "MyApp.Services.Users",
Language = "csharp",
Kind = ContainerKind.NAMESPACE,
DeclaredPackage = "MyApp.Services.Users",
NamespacePath = listOf("MyApp", "Services", "Users")
)

assertEquals("csharp", csharpContainer.Language)
assertEquals(ContainerKind.NAMESPACE, csharpContainer.Kind)
assertEquals(listOf("MyApp", "Services", "Users"), csharpContainer.NamespacePath)
}

@Test
fun `should support config container kind for Toml`() {
val tomlContainer = CodeContainer(
FullName = "Cargo.toml",
Language = "toml",
Kind = ContainerKind.CONFIG
)

assertEquals("toml", tomlContainer.Language)
assertEquals(ContainerKind.CONFIG, tomlContainer.Kind)
}

@Test
fun `should support build script container kind for CMake`() {
val cmakeContainer = CodeContainer(
FullName = "CMakeLists.txt",
Language = "cmake",
Kind = ContainerKind.BUILD_SCRIPT
)

assertEquals("cmake", cmakeContainer.Language)
assertEquals(ContainerKind.BUILD_SCRIPT, cmakeContainer.Kind)
}

@Test
fun `should support IDL container kind for Protobuf`() {
val protobufContainer = CodeContainer(
FullName = "user.proto",
PackageName = "com.example.api",
Language = "protobuf",
Kind = ContainerKind.IDL,
DeclaredPackage = "com.example.api"
)

assertEquals("protobuf", protobufContainer.Language)
assertEquals(ContainerKind.IDL, protobufContainer.Kind)
assertEquals("com.example.api", protobufContainer.DeclaredPackage)
}

@Test
fun `should maintain backward compatibility with legacy PackageName field`() {
// Old code that only uses PackageName should still work
val legacyContainer = CodeContainer(
FullName = "Test.java",
PackageName = "com.example"
)

assertEquals("com.example", legacyContainer.PackageName)
// New fields should have sensible defaults
assertEquals("", legacyContainer.Language)
assertEquals(ContainerKind.UNKNOWN, legacyContainer.Kind)
assertEquals("", legacyContainer.DeclaredPackage)
assertEquals("", legacyContainer.ResolvedModulePath)
assertEquals(emptyList<String>(), legacyContainer.NamespacePath)
}
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While unit tests have been added for the new fields in CodeContainerTest, there are no integration tests verifying that the parsers actually populate these fields correctly when parsing real code. Consider adding parser-level tests that verify Language, Kind, DeclaredPackage, ResolvedModulePath, and NamespacePath are correctly set for representative code samples in each supported language.

Copilot uses AI. Check for mistakes.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@build.gradle.kts`:
- Around line 204-231: The getMavenCredentials() helper currently returns
credentials from the first <server> entry unconditionally; update it to filter
by server id before returning credentials by reading
server.getElementsByTagName("id").item(0)?.textContent and comparing it against
an allowed set (e.g., "ossrh", "central", "sonatype-nexus-staging" or a
configurable list). In the loop over servers (variable servers, server), only
return Pair(username, password) when the id matches the allowed IDs; otherwise
continue searching and keep existing try/catch and logger.warn behavior if none
match.
- Around line 100-103: The snapshotsRepoUrl constant is pointing to the wrong
OSSRH endpoint; update the value of snapshotsRepoUrl to
"https://central.sonatype.com/repository/maven-snapshots/" so that the
conditional assignment to url (the existing url = if
(version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl)
uses the correct snapshots repository; leave releasesRepoUrl and the url
selection logic unchanged.

In `@chapi-ast-cpp/src/main/kotlin/chapi/ast/cppast/CPPBasicIdentListener.kt`:
- Around line 35-41: The enterNamespaceDefinition adds the namespace to
codeContainer.NamespacePath but there is no exitNamespaceDefinition to pop it,
causing NamespacePath to grow; add an override fun exitNamespaceDefinition(ctx:
CPP14Parser.NamespaceDefinitionContext?) that checks if
codeContainer.NamespacePath.isNotEmpty() then sets codeContainer.NamespacePath =
codeContainer.NamespacePath.dropLast(1); also consider clearing or resetting
codeContainer.PackageName and/or DeclaredPackage on exit for top-level namespace
resets if your semantics require it (mirror the pattern used in
TypeScriptFullIdentListener.kt).

@codecov
Copy link

codecov bot commented Jan 17, 2026

Codecov Report

❌ Patch coverage is 77.95699% with 82 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.58%. Comparing base (53139b7) to head (32ff90a).

Files with missing lines Patch % Lines
...i/ast/typescriptast/TypeScriptFullIdentListener.kt 33.33% 54 Missing and 2 partials ⚠️
...in/src/main/kotlin/chapi/domain/core/CodeExport.kt 76.00% 6 Missing ⚠️
...kotlin/chapi/ast/javaast/JavaBasicIdentListener.kt 82.14% 2 Missing and 3 partials ⚠️
...lin/chapi/ast/pythonast/PythonFullIdentListener.kt 89.13% 1 Missing and 4 partials ⚠️
...otlin/chapi/ast/scalaast/ScalaFullIdentListener.kt 83.33% 3 Missing and 1 partial ⚠️
...main/kotlin/chapi/ast/goast/GoFullIdentListener.kt 87.50% 0 Missing and 3 partials ⚠️
...in/kotlin/chapi/ast/csharpast/CSharpAstListener.kt 86.66% 1 Missing and 1 partial ⚠️
...in/src/main/kotlin/chapi/domain/core/CodeImport.kt 96.96% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master      #42      +/-   ##
============================================
+ Coverage     73.20%   73.58%   +0.37%     
- Complexity     1226     1249      +23     
============================================
  Files            69       69              
  Lines          4800     5106     +306     
  Branches        941      975      +34     
============================================
+ Hits           3514     3757     +243     
- Misses          714      769      +55     
- Partials        572      580       +8     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

This commit addresses Issue #41 problem 3: CodeImport/CodeExport being
too "string-based" to accurately represent different language import styles.

Changes to CodeImport:
- Add `ImportKind` enum: UNKNOWN, DEFAULT, NAMED, NAMESPACE, SIDE_EFFECT,
  STATIC, WILDCARD, RELATIVE, TYPE_ONLY, DOT
- Add `ImportSpecifier` data class with OriginalName, LocalName, IsTypeOnly
- New fields: Kind, Specifiers, DefaultName, NamespaceName, IsTypeOnly, PathSegments

Changes to CodeExport:
- Add `ExportKind` enum: UNKNOWN, DEFAULT, NAMED, RE_EXPORT_ALL,
  RE_EXPORT_NAMED, TYPE_ONLY
- Add `ExportSpecifier` data class with LocalName, ExportedName, IsTypeOnly
- New fields: Kind, Specifiers, FromSource, IsTypeOnly

Parser updates:
- TypeScript: full support for default/named/namespace/side-effect imports,
  default/named/re-export exports with specifiers
- Rust: PathSegments for use paths, WILDCARD for glob imports
- Python: NAMED/RELATIVE/WILDCARD imports with specifiers
- Java: STATIC/WILDCARD/NAMED imports with specifiers
- Scala: NAMED/WILDCARD imports with specifiers
- Go: DOT/SIDE_EFFECT/NAMED imports with specifiers

Backward compatibility:
- All legacy fields (Source, AsName, UsageName, Name, SourceFile) preserved
- New fields have sensible defaults

Partially addresses #41 (problem 3 of 5)
@phodal phodal changed the title feat(domain): add structured container semantics for cross-language clarity feat(domain): add structured container and import/export semantics for cross-language clarity Jan 17, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
chapi-ast-scala/src/main/kotlin/chapi/ast/scalaast/ScalaFullIdentListener.kt (1)

37-56: Unused specifiers accumulator list.

The specifiers list at line 37 is populated (line 44) but never used. Each CodeImport at line 50 creates its own inline listOf(ImportSpecifier(...)) instead of using the accumulated list. Either remove the unused specifiers list or use it to create a single CodeImport with all specifiers for grouped imports like import pkg.{A, B, C}.

Option 1: Remove unused list
             if (importSelectors != null) {
                 // Handle import with selectors: import org.apache.spark.sql.{DataFrame, Row, functions}
-                val specifiers = mutableListOf<ImportSpecifier>()
-                
                 importSelectors.importSelector().forEach { selector ->
                     val selectorName = selector.Id(0)?.text ?: "_"
                     val asName = if (selector.Id().size > 1) selector.Id(1).text else selectorName

                     if (selectorName != "_") {
-                        specifiers += ImportSpecifier(OriginalName = selectorName, LocalName = asName)
-                        
                         val codeImport = CodeImport(
🤖 Fix all issues with AI agents
In `@chapi-ast-java/src/main/kotlin/chapi/ast/javaast/JavaBasicIdentListener.kt`:
- Around line 21-51: enterImportDeclaration currently treats static imports
before checking for wildcard, and named imports don't set UsageName; update the
logic to first handle the static+wildcard case (if isStatic && isWildcard) so
that for "import static a.b.C.*;" you set codeImport.Kind = ImportKind.WILDCARD,
codeImport.UsageName = listOf("*"), codeImport.Source =
fullSource.substringBeforeLast(".*").removeSuffix(".*") or simply
fullSource.substringBeforeLast(".*").ifEmpty{fullSource.substringBeforeLast('.')
/*fallback*/} (resulting in "a.b.C"), and Specifiers = empty or null; then
handle static named imports (isStatic && !isWildcard) by setting UsageName to
the last segment and Source to dropLast(1) as you already do; finally ensure
non-static named imports (else branch) also populate codeImport.UsageName =
listOf(className) in addition to Specifiers and Kind = NAMED so
JavaFullIdentListener's imp.UsageName.contains(...) works; keep PathSegments
assignment as-is.

In `@chapi-ast-java/src/main/kotlin/chapi/ast/javaast/JavaFullIdentListener.kt`:
- Around line 77-84: Named imports currently don't set codeImport.UsageName
which breaks checks like warpTargetFullType()'s
imp.UsageName.contains(pureTargetType); in the else branch that sets
codeImport.Kind = ImportKind.NAMED and builds className and
codeImport.Specifiers, also populate codeImport.UsageName (e.g., set it to a
list containing className or the appropriate usage string) so named imports
mirror static/wildcard imports; update the block that constructs ImportSpecifier
(and/or codeImport.UsageName) to include the className in UsageName.
- Around line 64-76: Handle the static+wildcard import case before the exclusive
branches: if both isStatic and isWildcard are true, set codeImport.Source =
fullSource, codeImport.UsageName = listOf("*"), set codeImport.Scope = "static"
and set codeImport.Kind and Specifiers to reflect a static wildcard import (e.g.
Kind = ImportKind.WILDCARD or a combined value used in your model and Specifiers
= listOf(ImportSpecifier(OriginalName="*", LocalName="*"))), then return/skip
the other branches; update the logic around the isStatic/isWildcard checks in
JavaFullIdentListener (where fullSource, isStatic, isWildcard, codeImport,
ImportSpecifier and ImportKind are referenced) so static wildcard imports like
import static java.util.Arrays.* are recorded with Source = "java.util.Arrays"
and UsageName = ["*"] and Scope = "static".

In `@chapi-ast-rust/src/main/kotlin/chapi/ast/rustast/RustAstBaseListener.kt`:
- Around line 72-97: Move the glob and relative detection into the per-path loop
in enterUseDeclaration so each produced path from buildToPath is classified
correctly; for each path (in the imports.forEach), compute isGlob for that path
by checking if the path contains "*" or endsWith "*" (e.g. path.any { it == "*"
} or path.last() == "*"), and compute relative by checking the path's first
segment against the set {"crate","self","super"} instead of only "crate". Then
use these per-path booleans when constructing CodeImport (fields: Kind =
ImportKind.WILDCARD/RELATIVE/NAMED, Scope = "crate" when relative else "cargo",
and ensure Specifiers/AsName remain derived from the path). Reference
functions/types: enterUseDeclaration, buildToPath, imports, CodeImport,
ImportKind, ImportSpecifier.

In
`@chapi-ast-scala/src/main/kotlin/chapi/ast/scalaast/ScalaFullIdentListener.kt`:
- Around line 94-99: The code sets ImportSpecifier.OriginalName to asName for
non-wildcard imports, but OriginalName must be the exported/source identifier
(the name from the import path) while LocalName should remain the local/asName;
in ScalaFullIdentListener locate the non-wildcard branch that assigns
codeImport.Specifiers and change ImportSpecifier.OriginalName to the actual
source identifier parsed from the import path (the parsed import element, not
asName) and keep ImportSpecifier.LocalName = asName; ensure this uses the
variable that holds the source/imported name (instead of asName) when creating
the ImportSpecifier.
♻️ Duplicate comments (1)
chapi-ast-python/src/main/kotlin/chapi/ast/pythonast/PythonFullIdentListener.kt (1)

8-13: Container initialization with module semantics for Python.

The CodeContainer correctly uses ContainerKind.MODULE for Python files (as Python files are modules) and derives ResolvedModulePath from the filename. Note that the path derivation may not handle edge cases like filenames with multiple dots (e.g., test.utils.pytest.utils), as noted in a previous review.

🧹 Nitpick comments (5)
chapi-ast-typescript/src/main/kotlin/chapi/ast/typescriptast/TypeScriptFullIdentListener.kt (1)

1337-1343: Potential duplicate exports when adding to both nodes.

Adding the same export to both currentNode.Exports and defaultNode.Exports may cause duplicates in the final output. If currentNode refers to an active class/interface and defaultNode is also added to codeContainer.DataStructures (in getNodeInfo), the export will appear twice.

Consider adding only to defaultNode for top-level default exports, or conditionally adding based on whether we're inside a class context:

♻️ Suggested fix
     override fun enterExportDefaultDeclaration(ctx: TypeScriptParser.ExportDefaultDeclarationContext?) {
         // Get the exported expression/name
         val name = ctx?.singleExpression()?.text
         if (name != null) {
             val export = CodeExport(
                 Name = name,
                 Kind = ExportKind.DEFAULT
             )
-            currentNode.Exports += export
             defaultNode.Exports += export
         }
     }
chapi-ast-java/src/main/kotlin/chapi/ast/javaast/JavaFullIdentListener.kt (2)

51-55: Consider defensive null handling for qualifiedName().

While ctx is typically non-null in ANTLR listener enter methods, using ctx!!.qualifiedName()!!.text could cause a NullPointerException if the grammar encounters a malformed package declaration where qualifiedName() is null. The previous implementation may have had similar handling, but consider a defensive approach.

♻️ Suggested defensive handling
 override fun enterPackageDeclaration(ctx: JavaParser.PackageDeclarationContext?) {
-    val packageName = ctx!!.qualifiedName()!!.text
-    codeContainer.PackageName = packageName
-    codeContainer.DeclaredPackage = packageName
+    val packageName = ctx?.qualifiedName()?.text ?: return
+    codeContainer.PackageName = packageName
+    codeContainer.DeclaredPackage = packageName
 }

57-62: Consider defensive null handling for qualifiedName().

Similar to enterPackageDeclaration, using ctx!!.qualifiedName()!!.text could throw a NullPointerException for malformed import statements.

♻️ Suggested defensive handling
 override fun enterImportDeclaration(ctx: JavaParser.ImportDeclarationContext?) {
-    val fullSource = ctx!!.qualifiedName()!!.text
+    val fullSource = ctx?.qualifiedName()?.text ?: return
     val isStatic = ctx.STATIC() != null
     val isWildcard = ctx.MUL() != null
chapi-ast-python/src/main/kotlin/chapi/ast/pythonast/PythonFullIdentListener.kt (1)

95-97: AsName reflects only the last alias in multi-import statements.

In from x import a, b as c, only c is stored in AsName. This is consistent with the legacy single-value field design, but the full alias information is now correctly captured in Specifiers. Consider documenting this behavior or deprecating AsName in favor of Specifiers in future versions.

chapi-ast-rust/src/main/kotlin/chapi/ast/rustast/RustAstBaseListener.kt (1)

17-23: Consider emitting CRATE for crate roots.

Right now Kind is always MODULE, so lib.rs / main.rs files never surface as CRATE. If downstream consumers rely on that distinction, they’ll miss it.

♻️ Proposed adjustment
-        Kind = ContainerKind.MODULE,
+        Kind = if (fileName.endsWith(LIB_RS) || fileName.endsWith(MAIN_RS)) {
+            ContainerKind.CRATE
+        } else {
+            ContainerKind.MODULE
+        },

Comment on lines +64 to +76
if (isStatic) {
val sourceSplit = fullSource.split(".")
codeImport.UsageName = listOf(sourceSplit.last())

val split = sourceSplit.dropLast(1)
codeImport.Source = split.joinToString(".")
codeImport.Source = sourceSplit.dropLast(1).joinToString(".")
codeImport.Kind = ImportKind.STATIC
codeImport.Scope = "static"
codeImport.Specifiers = listOf(ImportSpecifier(
OriginalName = sourceSplit.last(),
LocalName = sourceSplit.last()
))
} else if (isWildcard) {
codeImport.Kind = ImportKind.WILDCARD
codeImport.UsageName = listOf("*")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Static wildcard imports handled incorrectly.

For imports like import static java.util.Arrays.*;, both isStatic and isWildcard are true. The current if-elif structure means the static branch executes exclusively, incorrectly treating it as a single static member import:

  • fullSource = "java.util.Arrays" (from qualifiedName())
  • Current logic sets Source = "java.util" and UsageName = ["Arrays"]
  • Should be: Source = "java.util.Arrays", UsageName = ["*"], Kind should reflect both static and wildcard
🐛 Proposed fix to handle static wildcard imports
     if (isStatic) {
-        val sourceSplit = fullSource.split(".")
-        codeImport.UsageName = listOf(sourceSplit.last())
-        codeImport.Source = sourceSplit.dropLast(1).joinToString(".")
-        codeImport.Kind = ImportKind.STATIC
-        codeImport.Scope = "static"
-        codeImport.Specifiers = listOf(ImportSpecifier(
-            OriginalName = sourceSplit.last(),
-            LocalName = sourceSplit.last()
-        ))
-    } else if (isWildcard) {
+        if (isWildcard) {
+            // Static wildcard import: import static pkg.Class.*
+            codeImport.UsageName = listOf("*")
+            codeImport.Source = fullSource
+            codeImport.Kind = ImportKind.STATIC  // or a new STATIC_WILDCARD kind
+            codeImport.Scope = "static"
+        } else {
+            // Single static member import: import static pkg.Class.member
+            val sourceSplit = fullSource.split(".")
+            codeImport.UsageName = listOf(sourceSplit.last())
+            codeImport.Source = sourceSplit.dropLast(1).joinToString(".")
+            codeImport.Kind = ImportKind.STATIC
+            codeImport.Scope = "static"
+            codeImport.Specifiers = listOf(ImportSpecifier(
+                OriginalName = sourceSplit.last(),
+                LocalName = sourceSplit.last()
+            ))
+        }
+    } else if (isWildcard) {
         codeImport.Kind = ImportKind.WILDCARD
         codeImport.UsageName = listOf("*")
🤖 Prompt for AI Agents
In `@chapi-ast-java/src/main/kotlin/chapi/ast/javaast/JavaFullIdentListener.kt`
around lines 64 - 76, Handle the static+wildcard import case before the
exclusive branches: if both isStatic and isWildcard are true, set
codeImport.Source = fullSource, codeImport.UsageName = listOf("*"), set
codeImport.Scope = "static" and set codeImport.Kind and Specifiers to reflect a
static wildcard import (e.g. Kind = ImportKind.WILDCARD or a combined value used
in your model and Specifiers = listOf(ImportSpecifier(OriginalName="*",
LocalName="*"))), then return/skip the other branches; update the logic around
the isStatic/isWildcard checks in JavaFullIdentListener (where fullSource,
isStatic, isWildcard, codeImport, ImportSpecifier and ImportKind are referenced)
so static wildcard imports like import static java.util.Arrays.* are recorded
with Source = "java.util.Arrays" and UsageName = ["*"] and Scope = "static".

- Improve import classification for Java static imports, Rust relative imports, and Scala import specifiers
- Fix Python module path resolution to handle file extensions correctly
- Add namespace tracking for C++ AST
- Update Sonatype snapshot repository URL and server ID filtering
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants