Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion bindgen/internal/emit/emitter.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package emit
import (
"fmt"
"slices"
"strconv"
"strings"

"github.com/ivov/lisette/bindgen/internal/config"
Expand Down Expand Up @@ -60,14 +61,26 @@ func NewEmitter(cfg *config.Config, pkgPath, pkgName string, bitFlagSetTypes, cl
func (e *Emitter) EmitHeader(pkg, pkgName, lisetteVersion, goVersion string) {
e.buf.WriteString("// Generated by Lisette bindgen\n")
fmt.Fprintf(&e.buf, "// Source: %s (%s)\n", pkg, packageSourceType(pkg))
if lastSegment := pkg[strings.LastIndex(pkg, "/")+1:]; pkgName != lastSegment {
// Lisette derives a Go import's local name from the segment before a `/vN`
// suffix, so a package actually named `vN` must declare `// Package:` itself.
if lastSegment := pkg[strings.LastIndex(pkg, "/")+1:]; pkgName != lastSegment || hasMajorVersionSuffix(pkg) {
fmt.Fprintf(&e.buf, "// Package: %s\n", pkgName)
}
fmt.Fprintf(&e.buf, "// Go: %s\n", strings.TrimPrefix(goVersion, "go"))
fmt.Fprintf(&e.buf, "// Lisette: %s\n", lisetteVersion)
e.buf.WriteString("\n")
}

// hasMajorVersionSuffix reports whether the path ends in a `/v2`+ major-version suffix.
func hasMajorVersionSuffix(pkgPath string) bool {
last := pkgPath[strings.LastIndex(pkgPath, "/")+1:]
if len(last) < 2 || last[0] != 'v' {
return false
}
major, err := strconv.Atoi(last[1:])
return err == nil && major >= 2
}

func packageSourceType(pkg string) string {
if strings.HasPrefix(pkg, "/") || strings.HasPrefix(pkg, "./") || strings.HasPrefix(pkg, "../") {
return "local"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Package v2 sits at a `/v2` path but is genuinely named `v2` (like the
// `k8s.io/api/.../v2` packages), so bindgen must emit an explicit
// `// Package: v2` directive even though it matches the last path segment.
package v2

// Config is a marker type to give the package an exported surface.
type Config struct {
Name string
}
10 changes: 10 additions & 0 deletions bindgen/tests/testdata/snapshots/major_version_self_named/v2.d.lis
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Generated by Lisette bindgen
// Source: ./testdata/fixtures/major_version_self_named/v2 (local)
// Package: v2
// Go: 0.0.0
// Lisette: 0.0.0

/// Config is a marker type to give the package an exported surface.
pub struct Config {
pub Name: string,
}
4 changes: 4 additions & 0 deletions crates/emit/src/analyze/facts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,10 @@ impl<'a> EmitFacts<'a> {
self.go_package_names
}

pub(crate) fn go_module_ids(&self) -> &'a HashSet<String> {
self.go_module_ids
}

pub(crate) fn has_global_exported_method_name(&self, method: &str) -> bool {
self.globals.exported_method_names.contains(method)
}
Expand Down
7 changes: 5 additions & 2 deletions crates/emit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -506,8 +506,11 @@ impl<'a> Planner<'a> {
}
}

let mut import_builder =
ImportBuilder::from_plan(&file_plan.imports, self.facts.go_package_names());
let mut import_builder = ImportBuilder::from_plan(
&file_plan.imports,
self.facts.go_package_names(),
self.facts.go_module_ids(),
);

self.drain_file_emission_into(&mut source);
fx.drain_into(&mut import_builder);
Expand Down
8 changes: 4 additions & 4 deletions crates/emit/src/names/resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,10 @@ impl Planner<'_> {
if let Some(pkg_name) = self.facts.go_package_name(module) {
return pkg_name.to_string();
}
let path = module
.strip_prefix(go_name::GO_IMPORT_PREFIX)
.unwrap_or(module);
go_name::go_package_name(path).to_string()
match module.strip_prefix(go_name::GO_IMPORT_PREFIX) {
Some(go_path) => syntax::program::go_import_default_name(go_path).to_string(),
None => go_name::go_package_name(module).to_string(),
}
}

pub(crate) fn qualify_method_call(
Expand Down
36 changes: 27 additions & 9 deletions crates/emit/src/output/imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ impl ImportPlan {

pub struct ImportBuilder<'a> {
go_package_names: &'a HashMap<String, String>,
go_module_ids: &'a HashSet<String>,
imports: HashMap<String, String>,
/// Generated imports of paths the source already imports under another
/// alias; emitted alongside (Go permits duplicate import paths).
Expand All @@ -60,9 +61,13 @@ pub struct ImportBuilder<'a> {
}

impl<'a> ImportBuilder<'a> {
pub fn new(go_package_names: &'a HashMap<String, String>) -> Self {
pub fn new(
go_package_names: &'a HashMap<String, String>,
go_module_ids: &'a HashSet<String>,
) -> Self {
Self {
go_package_names,
go_module_ids,
imports: HashMap::default(),
generated_duplicates: Vec::new(),
dropped_aliases: HashMap::default(),
Expand All @@ -73,9 +78,11 @@ impl<'a> ImportBuilder<'a> {
pub(crate) fn from_plan(
plan: &ImportPlan,
go_package_names: &'a HashMap<String, String>,
go_module_ids: &'a HashSet<String>,
) -> Self {
Self {
go_package_names,
go_module_ids,
imports: plan.imports.clone(),
generated_duplicates: Vec::new(),
dropped_aliases: plan.dropped_aliases.clone(),
Expand Down Expand Up @@ -108,7 +115,8 @@ impl<'a> ImportBuilder<'a> {
let canonical = package.qualifier();
self.used_modules.insert(path.to_string());
match self.imports.get(path) {
Some(alias) if effective_package_name(path, alias) == canonical => {}
Some(alias) if effective_package_name(path, alias, self.go_module_ids) == canonical => {
}
Some(_) => {
if !self.generated_duplicates.iter().any(|(p, _)| p == path) {
self.generated_duplicates
Expand All @@ -131,12 +139,15 @@ impl<'a> ImportBuilder<'a> {
entries.extend(self.generated_duplicates);
entries.sort();
entries.dedup();
let diagnostics = detect_collisions(&entries);
let diagnostics = detect_collisions(&entries, self.go_module_ids);
(entries, diagnostics)
}
}

fn detect_collisions(entries: &[(String, String)]) -> Vec<LisetteDiagnostic> {
fn detect_collisions(
entries: &[(String, String)],
go_module_ids: &HashSet<String>,
) -> Vec<LisetteDiagnostic> {
if entries.len() < 2 {
return Vec::new();
}
Expand All @@ -145,7 +156,7 @@ fn detect_collisions(entries: &[(String, String)]) -> Vec<LisetteDiagnostic> {
if alias == "_" {
continue;
}
let effective = effective_package_name(path, alias);
let effective = effective_package_name(path, alias, go_module_ids);
let sanitized = go_name::sanitize_package_name(effective).into_owned();
groups.entry(sanitized).or_default().push(path.as_str());
}
Expand All @@ -160,11 +171,18 @@ fn detect_collisions(entries: &[(String, String)]) -> Vec<LisetteDiagnostic> {
.collect()
}

fn effective_package_name<'a>(path: &'a str, alias: &'a str) -> &'a str {
if alias.is_empty() {
path.rsplit('/').next().unwrap_or(path)
fn effective_package_name<'a>(
path: &'a str,
alias: &'a str,
go_module_ids: &HashSet<String>,
) -> &'a str {
if !alias.is_empty() {
return alias;
}
if go_module_ids.contains(&format!("{}{path}", go_name::GO_IMPORT_PREFIX)) {
syntax::program::go_import_default_name(path)
} else {
alias
path.rsplit('/').next().unwrap_or(path)
}
}

Expand Down
1 change: 0 additions & 1 deletion crates/semantics/src/cache/go_stdlib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,6 @@ fn register_cached_go_module(

if let Some(source) = source
&& let Some(pkg_name) = extract_package_directive(source)
&& module_id.rsplit('/').next() != Some(pkg_name.as_str())
{
store
.go_package_names
Expand Down
15 changes: 4 additions & 11 deletions crates/semantics/src/checker/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ use ecow::EcoString;
use scopes::Scopes;
use syntax::ast::Visibility as AstVisibility;
use syntax::ast::{Annotation, Expression, Generic, ImportAlias, Span, StructFieldDefinition};
use syntax::program::{Definition, DefinitionBody, File, FileImport, MethodSignatures, Module};
use syntax::program::{
Definition, DefinitionBody, File, FileImport, MethodSignatures, Module, go_import_default_name,
};
use syntax::types::{SubstitutionMap, Symbol, Type, substitute};

pub use infer::expressions::comparison::check_not_comparable;
Expand Down Expand Up @@ -708,7 +710,7 @@ impl<'s> TaskState<'s> {
.go_package_names
.get(module_id)
.cloned()
.unwrap_or_else(|| go_module_last_segment(module_id).to_string());
.unwrap_or_else(|| go_import_default_name(module_id).to_string());
self.imports
.prefix_to_module
.insert(self_alias, module_id.into());
Expand Down Expand Up @@ -909,15 +911,6 @@ impl<'s> TaskState<'s> {
}
}

fn go_module_last_segment(module_id: &str) -> &str {
module_id
.strip_prefix("go:")
.unwrap_or(module_id)
.rsplit('/')
.next()
.unwrap_or(module_id)
}

/// Returns `true` if the given name is reserved and cannot be used as an import alias.
///
/// Reserved names include Go keywords, Go predeclared identifiers, Go builtins,
Expand Down
4 changes: 1 addition & 3 deletions crates/semantics/src/checker/registration/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,9 +276,7 @@ impl TaskState<'_> {
store.mark_visited(module_id);
store.add_module(module_id);

if let Some(pkg_name) = extract_package_directive(source)
&& module_id.rsplit('/').next() != Some(pkg_name.as_str())
{
if let Some(pkg_name) = extract_package_directive(source) {
store
.go_package_names
.insert(module_id.to_string(), pkg_name);
Expand Down
3 changes: 1 addition & 2 deletions crates/semantics/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,7 @@ pub struct Store {
pub module_ids: Vec<ModuleId>,
/// file ID -> module ID
pub files: HashMap<u32, String>,
/// Go module ID -> Go package name, from the typedef `// Package:` directive.
/// Present only when the package name differs from the final path segment.
/// Go module ID -> package name from the typedef `// Package:` directive.
pub go_package_names: HashMap<String, String>,
/// File ID -> on-disk path of the `.d.lis` typedef. Lets the LSP map go: typedef
/// file IDs to the actual cache path so go-to-definition can navigate there.
Expand Down
87 changes: 78 additions & 9 deletions crates/syntax/src/program/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,35 @@ impl FileImport {
if let Some(pkg_name) = go_package_names.get(self.name.as_str()) {
return Some(pkg_name.clone());
}
Some(
self.name
.strip_prefix("go:")
.unwrap_or(&self.name)
.split('/')
.next_back()
.unwrap_or(&self.name)
.to_string(),
)
let default = match self.name.strip_prefix("go:") {
Some(go_path) => go_import_default_name(go_path),
None => self.name.rsplit('/').next().unwrap_or(&self.name),
};
Some(default.to_string())
}
}
}
}

pub fn go_import_default_name(import_path: &str) -> &str {
let path = import_path.strip_prefix("go:").unwrap_or(import_path);
let mut segments = path.rsplit('/');
let last = segments.next().unwrap_or(path);
if is_major_version_segment(last)
&& let Some(preceding) = segments.next()
{
return preceding;
}
last
}

fn is_major_version_segment(segment: &str) -> bool {
segment
.strip_prefix('v')
.and_then(|digits| digits.parse::<u32>().ok())
.is_some_and(|major| major >= 2)
}

impl File {
pub fn new(
module_id: &str,
Expand Down Expand Up @@ -132,3 +147,57 @@ impl File {
.to_string()
}
}

#[cfg(test)]
mod tests {
use super::go_import_default_name;

#[test]
fn major_version_suffix_resolves_to_preceding_segment() {
assert_eq!(go_import_default_name("github.com/pion/sdp/v3"), "sdp");
assert_eq!(
go_import_default_name("go:github.com/pion/webrtc/v4"),
"webrtc"
);
assert_eq!(
go_import_default_name("go:github.com/pion/transport/v4"),
"transport"
);
}

#[test]
fn non_version_last_segment_is_kept() {
assert_eq!(go_import_default_name("go:strings"), "strings");
assert_eq!(
go_import_default_name("go:github.com/pion/datachannel"),
"datachannel"
);
assert_eq!(
go_import_default_name("go:github.com/pion/transport/v4/packetio"),
"packetio"
);
}

#[test]
fn v0_and_v1_are_ordinary_segments() {
assert_eq!(go_import_default_name("go:k8s.io/api/core/v1"), "v1");
assert_eq!(go_import_default_name("go:example.com/pkg/v0"), "v0");
}

#[test]
fn dotted_version_suffix_is_not_a_major_version_segment() {
assert_eq!(go_import_default_name("go:gopkg.in/yaml.v3"), "yaml.v3");
}

#[test]
fn version_like_segment_without_preceding_segment_is_kept() {
assert_eq!(go_import_default_name("v2"), "v2");
assert_eq!(go_import_default_name("go:v2"), "v2");
}

#[test]
fn bare_v_or_non_numeric_is_not_a_version() {
assert_eq!(go_import_default_name("go:example.com/foo/v"), "v");
assert_eq!(go_import_default_name("go:example.com/foo/vx"), "vx");
}
}
2 changes: 1 addition & 1 deletion crates/syntax/src/program/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ pub use emit_input::{
EmitInput, EqualityIndex, EqualityInfo, EqualityUnusableReason, MutationInfo, TestFunction,
TestIndex, UnusedInfo,
};
pub use file::{File, FileImport};
pub use file::{File, FileImport, go_import_default_name};
pub use module::{Module, ModuleId, ModuleInfo};
pub use resolution::{CallKind, DotAccessKind, NativeTypeKind, ReceiverCoercion};
Loading
Loading