Repository: pulumi/pulumi
Pull request #19569: Add `DocLanguageHelper.GetModuleName`
Author: login: iwahbe
Labels: name: impact/no-changelog-required
description: This issue doesn't require a CHANGELOG update
Created: 2025-05-16T09:51:07Z | Merged: 2025-05-16T14:27:31Z

This PR includes the new method, and an implementation for every language in this repo.

Motivated by https://github.com/pulumi/pulumi-service/issues/27266: The API we want to support needs needs to provide an overview of whats in the package (resources and functions). Concretely, that means it needs to return a list of modules, where each module needs to itself have a list of the resources and functions it contains. Each element returned (module, resource, function) needs to have a language appropriate name for each language we support.

I already have PRs to implement this in Java and YAML:

- https://github.com/pulumi/pulumi-java/pull/1808
- https://github.com/pulumi/pulumi-yaml/pull/797


Diff:
diff --git a/pkg/codegen/docs.go b/pkg/codegen/docs.go
index 6994895d973a..8f481200b23b 100644
--- a/pkg/codegen/docs.go
+++ b/pkg/codegen/docs.go
@@ -23,6 +23,14 @@ import (
 // DocLanguageHelper is an interface for extracting language-specific information from a Pulumi schema.
 // See the implementation for this interface under each of the language code generators.
 type DocLanguageHelper interface {
+	// GetModuleName returns the in-language name of the module.
+	//
+	// For example, lets get the hypothetical name of the module for the "pkg:module/nestedMod:Type" token in python:
+	//
+	//	var python python_codegen.DocLanguageHelper
+	//	python.GetModuleName(pkgRef, pkgRef.TokenToModule("pkg:module/nestedMod:Type")) // "module.nestedmod"
+	GetModuleName(pkg schema.PackageReference, modName string) string
+
 	GetPropertyName(p *schema.Property) (string, error)
 	GetEnumName(e *schema.Enum, typeName string) (string, error)
 	// GetTypeName gets the name of a type in the language of the DocLanguageHelper.
diff --git a/pkg/codegen/docs_integration_test.go b/pkg/codegen/docs_integration_test.go
index aa8ba3291e0d..3c78dd76fe95 100644
--- a/pkg/codegen/docs_integration_test.go
+++ b/pkg/codegen/docs_integration_test.go
@@ -516,8 +516,6 @@ func TestGetMethodResultName_NoImporter(t *testing.T) {
 		dotnet: "Foo.GetKubeconfigResult",
 	}
 
-	// Code generation is not safe to parallelize since import binding mutates the
-	// [schema.Package].
 	for lang, expected := range expected {
 		testDocsGenHelper(t, lang, pkg.Reference(), func(t *testing.T, helper codegen.DocLanguageHelper) {
 			actual := helper.GetMethodResultName(pkg.Reference(), "",
@@ -529,6 +527,137 @@ func TestGetMethodResultName_NoImporter(t *testing.T) {
 	}
 }
 
+func TestGetModuleName(t *testing.T) {
+	t.Parallel()
+
+	pkg := func(t *testing.T, language map[string]schema.RawMessage) schema.PackageReference {
+		schemaSpec := schema.PackageSpec{
+			Name: "example",
+			Resources: map[string]schema.ResourceSpec{
+				"example:index:Foo": {
+					IsComponent: true,
+					Methods: map[string]string{
+						"getKubeconfig": "example:index:Foo/getKubeconfig",
+					},
+				},
+			},
+			Functions: map[string]schema.FunctionSpec{
+				"example:index:Foo/getKubeconfig": {
+					Inputs: &schema.ObjectTypeSpec{
+						Properties: map[string]schema.PropertySpec{
+							"__self__": {
+								TypeSpec: schema.TypeSpec{
+									Ref: "#/resources/example:index:Foo",
+								},
+							},
+						},
+						Required: []string{"__self__"},
+					},
+					Outputs: &schema.ObjectTypeSpec{
+						Properties: map[string]schema.PropertySpec{
+							"kubeconfig": {
+								TypeSpec: schema.TypeSpec{
+									Type: "string",
+								},
+							},
+						},
+						Required: []string{"kubeconfig"},
+					},
+				},
+			},
+			Language: language,
+		}
+
+		return bind(t, schemaSpec)
+	}
+
+	tests := []struct {
+		name     string
+		schema   schema.PackageReference
+		token    string
+		expected map[language]string
+	}{
+		{
+			name:   "index",
+			schema: pkg(t, nil),
+			token:  "example:index:Foo",
+			expected: map[language]string{
+				golang: "",
+				nodejs: "",
+				python: "",
+				dotnet: "",
+			},
+		},
+		{
+			name:   "simple",
+			schema: pkg(t, nil),
+			token:  "example:mymodule:Foo",
+			expected: map[language]string{
+				golang: "mymodule",
+				nodejs: "mymodule",
+				python: "mymodule",
+				dotnet: "Mymodule",
+			},
+		},
+		{
+			name:   "nested",
+			schema: pkg(t, nil),
+			token:  "example:my/module:Foo",
+			expected: map[language]string{
+				golang: "my/module",
+				nodejs: "my.module",
+				python: "my/module",
+				dotnet: "My.Module",
+			},
+		},
+		{
+			name: "overwritten",
+			schema: pkg(t, map[string]schema.RawMessage{
+				"go": marshalIntoRaw(t, golang_codegen.GoPackageInfo{
+					ModuleToPackage: map[string]string{
+						"shouldoverride": "gopkg",
+					},
+				}),
+				"csharp": marshalIntoRaw(t, dotnet_codegen.CSharpPackageInfo{
+					Namespaces: map[string]string{
+						"shouldoverride": "DotNetPkg",
+					},
+				}),
+				"nodejs": marshalIntoRaw(t, nodejs_codegen.NodePackageInfo{
+					ModuleToPackage: map[string]string{
+						"shouldoverride": "nodepkg",
+					},
+				}),
+				"python": marshalIntoRaw(t, python_codegen.PackageInfo{
+					ModuleNameOverrides: map[string]string{
+						"shouldoverride": "pythonpkg",
+					},
+				}),
+			}),
+			token: "example:shouldoverride:Foo",
+			expected: map[language]string{
+				golang: "gopkg",
+				nodejs: "nodepkg",
+				python: "pythonpkg",
+				dotnet: "DotNetPkg",
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			assert.NotEmpty(t, tt.expected)
+			for lang, expected := range tt.expected {
+				testDocsGenHelper(t, lang, tt.schema, func(t *testing.T, helper codegen.DocLanguageHelper) {
+					actual := helper.GetModuleName(tt.schema, tt.schema.TokenToModule(tt.token))
+					assert.Equal(t, expected, actual)
+				})
+			}
+		})
+	}
+}
+
 func testDocsGenHelper(
 	t *testing.T, language language, schema schema.PackageReference,
 	f func(*testing.T, codegen.DocLanguageHelper),
diff --git a/pkg/codegen/dotnet/doc.go b/pkg/codegen/dotnet/doc.go
index 99bc73dcc04f..abedd125fefb 100644
--- a/pkg/codegen/dotnet/doc.go
+++ b/pkg/codegen/dotnet/doc.go
@@ -102,6 +102,12 @@ func (d DocLanguageHelper) GetMethodName(m *schema.Method) string {
 	return Title(m.Name)
 }
 
+func (d DocLanguageHelper) GetModuleName(pkg schema.PackageReference, module string) string {
+	a, _ := pkg.Language("csharp")
+	info, _ := a.(CSharpPackageInfo)
+	return namespaceName(info.Namespaces, module)
+}
+
 func (d DocLanguageHelper) GetMethodResultName(pkg schema.PackageReference, modName string, r *schema.Resource,
 	m *schema.Method,
 ) string {
diff --git a/pkg/codegen/go/doc.go b/pkg/codegen/go/doc.go
index 02366d6e3679..c03f46e9db1e 100644
--- a/pkg/codegen/go/doc.go
+++ b/pkg/codegen/go/doc.go
@@ -88,6 +88,10 @@ func (d DocLanguageHelper) GetDocLinkForFunctionInputOrOutputType(pkg *schema.Pa
 	return link + "Args"
 }
 
+func (d DocLanguageHelper) GetModuleName(pkg schema.PackageReference, module string) string {
+	return moduleToPackage(d.goPkgInfo.ModuleToPackage, module)
+}
+
 // GetLanguageTypeString returns the Go-specific type given a Pulumi schema type.
 func (d DocLanguageHelper) GetTypeName(pkg schema.PackageReference, t schema.Type, input bool, relativeToModule string) string {
 	goPkg := moduleToPackage(d.goPkgInfo.ModuleToPackage, relativeToModule)
diff --git a/pkg/codegen/nodejs/doc.go b/pkg/codegen/nodejs/doc.go
index ecff957a069f..5b32e2e6e9b1 100644
--- a/pkg/codegen/nodejs/doc.go
+++ b/pkg/codegen/nodejs/doc.go
@@ -68,6 +68,10 @@ func (d DocLanguageHelper) GetDocLinkForFunctionInputOrOutputType(pkg *schema.Pa
 	return d.GetDocLinkForResourceInputOrOutputType(pkg, modName, typeName, input)
 }
 
+func (d DocLanguageHelper) GetModuleName(pkg schema.PackageReference, module string) string {
+	return moduleName(module, pkg)
+}
+
 // GetLanguageTypeString returns the language-specific type given a Pulumi schema type.
 func (d DocLanguageHelper) GetTypeName(pkg schema.PackageReference, t schema.Type, input bool, relativeToModule string) string {
 	// Remove the union with `undefined` for optional types,
diff --git a/pkg/codegen/python/doc.go b/pkg/codegen/python/doc.go
index 24a8cabee2d2..f9f4d2192e85 100644
--- a/pkg/codegen/python/doc.go
+++ b/pkg/codegen/python/doc.go
@@ -73,6 +73,15 @@ func (d DocLanguageHelper) GetDocLinkForFunctionInputOrOutputType(pkg *schema.Pa
 	return ""
 }
 
+func (d DocLanguageHelper) GetModuleName(pkg schema.PackageReference, module string) string {
+	var info PackageInfo
+	if a, err := pkg.Language("python"); err == nil {
+		info, _ = a.(PackageInfo)
+	}
+
+	return moduleToPythonModule(module, info.ModuleNameOverrides)
+}
+
 // GetLanguageTypeString returns the Python-specific type given a Pulumi schema type.
 func (d DocLanguageHelper) GetTypeName(pkg schema.PackageReference, t schema.Type, input bool, relativeToModule string) string {
 	var info PackageInfo


Reviews:
user: login: davidemassarenti-optio3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T13:09:31Z
user: login: davidemassarenti-optio3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T13:10:35Z
user: login: davidemassarenti-optio3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T13:11:40Z
user: login: iwahbe
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T13:13:58Z
user: login: iwahbe
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T13:14:12Z
user: login: davidemassarenti-optio3
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T13:17:07Z
user: login: iwahbe
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T13:17:59Z
user: login: iwahbe
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T13:19:50Z
user: login: davidemassarenti-optio3
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T13:21:37Z
user: login: tgummerer
body: > I expanded the PR description to explain how this will be used.

Thanks, that's helpful!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-16T13:36:55Z

Review comments:
diff_hunk: @@ -102,6 +102,12 @@ func (d DocLanguageHelper) GetMethodName(m *schema.Method) string {
 	return Title(m.Name)
 }
 
+func (d DocLanguageHelper) GetModuleName(pkg schema.PackageReference, module string) string {
+	a, _ := pkg.Language("csharp")
+	info, _ := a.(CSharpPackageInfo)
path: pkg/codegen/dotnet/doc.go
user: login: davidemassarenti-optio3
body: Maybe check if the cast succeeded? Just in case something goes horribly wrong 
created_at: 2025-05-16T13:09:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -102,6 +102,12 @@ func (d DocLanguageHelper) GetMethodName(m *schema.Method) string {
 	return Title(m.Name)
 }
 
+func (d DocLanguageHelper) GetModuleName(pkg schema.PackageReference, module string) string {
+	a, _ := pkg.Language("csharp")
+	info, _ := a.(CSharpPackageInfo)
path: pkg/codegen/dotnet/doc.go
user: login: davidemassarenti-optio3
body: Technically, it's not a pointer so nothing crashes
created_at: 2025-05-16T13:10:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -102,6 +102,12 @@ func (d DocLanguageHelper) GetMethodName(m *schema.Method) string {
 	return Title(m.Name)
 }
 
+func (d DocLanguageHelper) GetModuleName(pkg schema.PackageReference, module string) string {
+	a, _ := pkg.Language("csharp")
path: pkg/codegen/dotnet/doc.go
user: login: davidemassarenti-optio3
body: Use the same pattern as in Python?

	if a, err := pkg.Language("python"); err == nil {
		info, _ = a.(PackageInfo)
	}
created_at: 2025-05-16T13:11:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -102,6 +102,12 @@ func (d DocLanguageHelper) GetMethodName(m *schema.Method) string {
 	return Title(m.Name)
 }
 
+func (d DocLanguageHelper) GetModuleName(pkg schema.PackageReference, module string) string {
+	a, _ := pkg.Language("csharp")
+	info, _ := a.(CSharpPackageInfo)
path: pkg/codegen/dotnet/doc.go
user: login: iwahbe
body: If a cast `t, ok := v.(T)` fails, we get `T{}, false`. L107 is semantically equivalent to:
```go
var info CSharpPackageInfo
if castResult, ok := a.(CSharpPackageInfo); ok {
    info = castResult
}
```

Likewise for L106, `pkg.Language(someString)` returns either a usable value `a` of type `any` or the zero value of `any` (`nil`) and a non-nil error. This code handles both.
created_at: 2025-05-16T13:13:58Z
author_association: MEMBER
diff_hunk: @@ -102,6 +102,12 @@ func (d DocLanguageHelper) GetMethodName(m *schema.Method) string {
 	return Title(m.Name)
 }
 
+func (d DocLanguageHelper) GetModuleName(pkg schema.PackageReference, module string) string {
+	a, _ := pkg.Language("csharp")
path: pkg/codegen/dotnet/doc.go
user: login: iwahbe
body: See https://github.com/pulumi/pulumi/pull/19569#discussion_r2093027362.

I used a different pattern in Python becuase that matches the patter already found in the `python` package's `doc.go`.
created_at: 2025-05-16T13:14:11Z
author_association: MEMBER
diff_hunk: @@ -102,6 +102,12 @@ func (d DocLanguageHelper) GetMethodName(m *schema.Method) string {
 	return Title(m.Name)
 }
 
+func (d DocLanguageHelper) GetModuleName(pkg schema.PackageReference, module string) string {
+	a, _ := pkg.Language("csharp")
path: pkg/codegen/dotnet/doc.go
user: login: davidemassarenti-optio3
body: Shouldn't we be consistent across languages? Either check everywhere or nowhere
created_at: 2025-05-16T13:17:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -102,6 +102,12 @@ func (d DocLanguageHelper) GetMethodName(m *schema.Method) string {
 	return Title(m.Name)
 }
 
+func (d DocLanguageHelper) GetModuleName(pkg schema.PackageReference, module string) string {
+	a, _ := pkg.Language("csharp")
+	info, _ := a.(CSharpPackageInfo)
path: pkg/codegen/dotnet/doc.go
user: login: iwahbe
body: This is the same pattern used on L109 (+L114).
created_at: 2025-05-16T13:17:59Z
author_association: MEMBER
diff_hunk: @@ -102,6 +102,12 @@ func (d DocLanguageHelper) GetMethodName(m *schema.Method) string {
 	return Title(m.Name)
 }
 
+func (d DocLanguageHelper) GetModuleName(pkg schema.PackageReference, module string) string {
+	a, _ := pkg.Language("csharp")
path: pkg/codegen/dotnet/doc.go
user: login: iwahbe
body: Yes... but we are not. I'd rather be consistent within a language then inconstant in all cases, and I'm not going to standardize all languages in this PR.
created_at: 2025-05-16T13:19:50Z
author_association: MEMBER

PR comments:
user: login: tgummerer
created_at: 2025-05-16T13:21:50Z
body: > Motivated by https://github.com/pulumi/pulumi-service/issues/27266

Not sure I understand the link between this and the linked issue.  I think the design doc hints at it, but it would be helpful to have a better description of what this is needed for in this PR.
author_association: COLLABORATOR
user: login: iwahbe
created_at: 2025-05-16T13:30:01Z
body: > > Motivated by [pulumi/pulumi-service#27266](https://github.com/pulumi/pulumi-service/issues/27266)
> 
> Not sure I understand the link between this and the linked issue. I think the design doc hints at it, but it would be helpful to have a better description of what this is needed for in this PR.

I expanded the PR description to explain how this will be used.
author_association: MEMBER