@@ -159,7 +159,32 @@ private class StubRenderer(private val bound: List<DeclarationModel>) {
159159 entries.flatMap { d -> leafOf(d).let { leaf -> if (isSuffixed(leaf)) listOf (leaf, baseOf(leaf)) else listOf (leaf) } }.toSet()
160160 }
161161
162- private val moduleNames: Set <String > = byModule.keys
162+ /* *
163+ * Module prefixes (every module and every directory above it) that name one directory on a
164+ * case-insensitive filesystem together with a sibling (issue #44). In each such group the
165+ * all-lower-case spelling keeps its path -- a Kotlin package -- and every other spelling is a
166+ * class module that is emitted inside its parent package's `__init__.pyi` instead.
167+ */
168+ private val foldedPrefixes: Set <String > = run {
169+ val prefixes = byModule.keys.flatMap { module ->
170+ val parts = module.split(' .' )
171+ parts.indices.map { parts.take(it + 1 ).joinToString(" ." ) }
172+ }.toSet()
173+ prefixes.groupBy { it.lowercase() }.values
174+ .filter { it.size > 1 }
175+ .flatten()
176+ .filter { ' .' in it && it.substringAfterLast(' .' ) != it.substringAfterLast(' .' ).lowercase() }
177+ .toSet()
178+ }
179+
180+ /* * The shortest folded prefix of [module] (the module itself included), or `null` if it keeps its own path. */
181+ private fun foldedPrefixOf (module : String ): String? {
182+ val parts = module.split(' .' )
183+ return parts.indices.map { parts.take(it + 1 ).joinToString(" ." ) }.firstOrNull { it in foldedPrefixes }
184+ }
185+
186+ /* * The modules that are still files: a folded class module is a class, not a module, for `classReference`. */
187+ private val moduleNames: Set <String > = byModule.keys.filter { foldedPrefixOf(it) == null }.toSet()
163188
164189 private val supertypes: Map <String , List <String >> = bound
165190 .filter { it.returnSupertypes.isNotEmpty() }
@@ -283,6 +308,12 @@ private class StubRenderer(private val bound: List<DeclarationModel>) {
283308
284309 private fun renderDef (d : DeclarationModel , ctx : ModuleOut ): String {
285310 val leaf = leafOf(d)
311+ // A Kotlin name that is a Python keyword (Compose's `Shadow.None`) cannot be written as an
312+ // attribute or a def -- `None: Shadow` makes the whole file a syntax error. Leave it out and
313+ // say how to reach it; the runtime still serves it under its Kotlin name.
314+ if (leaf in PYTHON_KEYWORDS ) {
315+ return " # Kotlin: ${kotlinSignatureOf(d)} -- '$leaf ' is a Python keyword; reach it with getattr(..., '$leaf ')"
316+ }
286317 if (d.kind == " STATIC_GETTER" ) {
287318 return buildString {
288319 appendLine(" $leaf : ${result(d, ctx)} " )
@@ -297,7 +328,8 @@ private class StubRenderer(private val bound: List<DeclarationModel>) {
297328 }
298329
299330 private fun overloadDefs (name : String , group : List <DeclarationModel >, ctx : ModuleOut ): String =
300- group.withIndex().joinToString(" \n " ) { (index, d) ->
331+ if (name in PYTHON_KEYWORDS ) " # '$name ' is a Python keyword; reach it with getattr(..., '$name ')"
332+ else group.withIndex().joinToString(" \n " ) { (index, d) ->
301333 val signature = " def $name (${parameters(d, ctx, method = false )} ) -> ${result(d, ctx)} : ..." + shadowed(index)
302334 if (group.size > 1 ) " @$TYPING .overload\n $signature " else signature
303335 }
@@ -313,6 +345,11 @@ private class StubRenderer(private val bound: List<DeclarationModel>) {
313345
314346 private fun renderModuleDefs () {
315347 byModule.forEach { (module, entries) ->
348+ val folded = foldedPrefixOf(module)
349+ if (folded != null ) {
350+ renderFoldedModule(module, folded, entries)
351+ return @forEach
352+ }
316353 val ctx = out (module)
317354 val sorted = entries.sortedBy { it.bindingName }
318355 sorted.forEach { ctx.defs + = renderDef(it, ctx) }
@@ -327,6 +364,32 @@ private class StubRenderer(private val bound: List<DeclarationModel>) {
327364 }
328365 }
329366
367+ /* *
368+ * A class module that collides case-insensitively with a sibling path (issue #44): its functions
369+ * become static members of the class of that name in the parent package's module, nested for the
370+ * parts of [module] below [folded]. The runtime import path is unchanged; only the stub layout is.
371+ * If the parent already defines a function of that name, the class cannot be declared beside it
372+ * (the runtime resolves the name to the function, as for `classReference`), so nothing is emitted.
373+ */
374+ private fun renderFoldedModule (module : String , folded : String , entries : List <DeclarationModel >) {
375+ val parentModule = folded.substringBeforeLast(' .' )
376+ val className = folded.substringAfterLast(' .' )
377+ if (className in takenNames[parentModule].orEmpty()) return
378+ val ctx = out (parentModule)
379+ val path = listOf (className) + module.removePrefix(folded).split(' .' ).filter { it.isNotEmpty() }
380+ val node = ctx.node(path)
381+ val sorted = entries.sortedBy { it.bindingName }
382+ val defs = sorted.map { renderDef(it, ctx) }
383+ val unsuffixedLeaves = sorted.filter { ! isSuffixed(leafOf(it)) }.map { leafOf(it) }.toSet()
384+ val overloads = sorted.filter { it.kind != " STATIC_GETTER" && isSuffixed(leafOf(it)) }
385+ .groupBy { baseOf(leafOf(it)) }
386+ .toSortedMap()
387+ .mapNotNull { (base, group) -> if (base !in unsuffixedLeaves) overloadDefs(base, group, ctx) else null }
388+ (defs + overloads).forEach { text ->
389+ node.members + = text.lines().joinToString(" \n " ) { if (it.startsWith(" def " )) " @staticmethod\n $it " else it }
390+ }
391+ }
392+
330393 // --------------------------------------------------------------------------- classes
331394
332395 /* * The class a declaration's extension receiver names, when the runtime attaches it as a method. */
@@ -398,7 +461,9 @@ private class StubRenderer(private val bound: List<DeclarationModel>) {
398461 node.bases?.let { into.append(' (' ).append(it).append(' )' ) }
399462 into.appendLine(" :" )
400463 into.append(indent).append(" \"\"\" " ).append(node.doc ? : " Kotlin: container of nested classes" ).appendLine(" \"\"\" " )
401- node.members.forEach { into.append(indent).append(" " ).appendLine(it) }
464+ node.members.forEach { member ->
465+ member.lines().forEach { into.append(indent).append(" " ).appendLine(it) }
466+ }
402467 node.children.values.forEach { renderNode(it, " $indent " , into) }
403468 }
404469
0 commit comments