Skip to content

Namespaces, nested static declarations and nested scope imports. #4324

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
lrhn opened this issue Apr 11, 2025 · 5 comments
Open

Namespaces, nested static declarations and nested scope imports. #4324

lrhn opened this issue Apr 11, 2025 · 5 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@lrhn
Copy link
Member

lrhn commented Apr 11, 2025

This is an attempt to combine a number of scope-nesting features.

Namespaces

Declare something which is only a namespace:

namespace Name {
  // members
}

It works almost exactly the same as extension Name on Never { ... } except that you don't have to write static in front of the declarations.
You access members by writing Name.foo, or just foo from inside the members themselves, where the declaration is in the lexical scope.

A class, mixin, enum, extension and extension type declaration also introduce a namespace where static members are available, which are accessed directly as Name.foo or AliasName.foo, and indirectly by dot-shorthands. This declares a namespace without a type, and without any notion of instantiation (it's not modules, and it cannot be generic), so all members behave like static members would in the other namespaces. Like an extension it doesn't introduce a type, so it can't be used with dot-shorthands or aliases.

The members of a namespace declaration counts as static declarations, even without a leading static.
It cannot contain any instance members or constructors.

Static nested declarations.

Inside a namespace-introducing declartion, you can also declare class, mixin, enum, extension type, extension, namespace and typedef declarations. In anything but a namespace declaration, those must be prefixed by static.

Example:

class C {
  static class D {
    static int bar => foo();
  }
  static namespace Names {
    const int x = 42;
  }
  static int foo() => Names.x;
}

The introduced declarations are just like any other declaration of that kind, the only difference is that the body scope of the outer declaration is in scope for the inner declaration. The inner declaration cannot refer to any instance member or type parameter of the outer declaration, because it is itself static.

The scope of the declarations is only the body-scope they are declared in.

For extension declarations, if they are privately named, they are only considered available inside the body-scope where they are declared (like a top-level private extension is only availabe to that library).
If an extension declaration has a public name, but is nested inside a privately-named declaration, then it's only available inside the body-scope of the nearest enclosing privately-named declaration.
If an extension declaration has a public name as is not inside any privately named namespace, then it's available everywhere in the library, and to libraries importing the declaring library and not hiding any of the namespaces enclosing the declaration.

(This is effectively just namespacing. The example above can be desugared to:

class C { 
  static int foo => _$Names.x;
}
class _$C$D {
  static int bar => C.foo();
}
namespace _$C$Names {
   const int x = 42;
}

where any reference to C.D is replaced by _$C$D, and C.Names by _$C$Names, whether explicit or as unqualified D or Names where the declarations would be in scope.)

Showing and hiding nested declarations.

An import or export directive can use qualified names in show/hide modifiers:

import "foo.dart" show C.D;
import "bar.dart" hide C.Names;

A qualifiied show makes every name in the chain available. If the libraries match the C/D/Names examples above, show C.D makes C available, with an nested namespace where only C.D is included, not C.Names. That means that the names available in static namespace is not uniquely defined by the declaration, each import can choose to modify that, introducing a nested import-scope that only contains some of the members.

The hide C.Names does not hide C, only C.Names. Hiding C would hide every member of C.

(Maybe have a way to hide/show multiple members of a namespace, show C.{foo,D} or show C..foo..D?)

Scoped import.

Allow "opening" a scope locally in another scope, introducing all the names of the opened scope.
You can open a namespace scope or an import (prefix) scope.

I would like to use import, but with import shorthands, import id; already means something else.
So, strawman: import <qid>;. The qualified ID must denote a namespace or import scope (which may or may not just be the same thing - a namespace).
Can be followed by as id and show/hide modifiers too. In the former case it introduces a new locally -scoped import namespace, and imports into that. Without as id, the names are imported into the current scope.

import "foo.dart" as c;

enum C { getsInTheWay; }
class E {
  static import <c.C.D>; // imports 'bar'
  int qux() => bar();  // `bar` refers to the same declaration as `c.C.D.bar`.
}

Can be used as static import <qid> in namespace-introducing declarations, without that static in namespace declarations and at top-level, fx as:

import "foo.dart" as c;
import <c> hide C.Names; // Into current (library import) scope.

If a scoped import includes an extension declaration which is not inside a privately named declaration, then the extension is available in the scope of the import.

Scoped export.

A static namespace can also export declarations from other namespaces.

import "foo.dart" as c;
export <c.C> show D; // Exports `C.D` from `foo.dart` as `D`.
class E {
  // Exports the static `x` declaration from `C` through `E`'s namespace.
  static export <c.C> show x;
}
void main() { print(E.x); } // same as `print(c.C.x);`.

Local declarations (inside code)

A typedef can be declared inside a function body, anywhere a statement or local declaration could be written. The scope of the name is the current block, and it can be references only after the declaration.

An import <qid> can be declared inside a function body, anywhere a statement or local declaration could be written. The scope of the imported names (or the prefix name if prefixed) is the current block, and it can be references only after the declaration.

If a local import includes an extension declaration which is not inside a privately named declaration, then the extension is available in the scope of the import.

Summary

You can introduce plain namespaces, like existing static namespaces or import scopes, without them also being something more. Such namespaces only contain static (including top-level) declarations.

Such namespaces, including existing class-like declarations, can contain static type, type-alias and extension and namespace declarations too. Namespace nesting is not limited to one level any more.

You can import declarations from one namespace into another, making them available to code in that namespace. If you import a a namespace containing nested namespaces, you get nested import namespaces for those. Each member can be shown/hidden.
You can export declarations from one namespace also from another namespace, making them available for accessing through the namespace. Can also show/hide as desired.

You can declare local typedefs and have namespace imports inside function bodies too.

This allows complete control over your own namespaces, while also allowing you to nest namespaces that you expose as deeply as desired.

(Protobuf compilers can nest declarations, so you can write Foo.Bar to access the Bar class nested inside Foo, but you can also just import <Foo> show Bar; and access it directly.)

@lrhn lrhn added the feature Proposed language feature that solves one or more problems label Apr 11, 2025
@rrousselGit
Copy link

That feels like a lot. In practice, I'm not sure we need anything more than just static classes/typedefs

namespace would be an abstract class with no constructor (like how we do things today):

class Foo {
  abstract MyNamespace {
    MyNamespace._();
    static const value = 42;
  }
}

Nested exports could be typedefs:

import 'a.dart' as my_import;
class Foo {
  typedef A = my_import.A;
}

And nested imports sound confusing.

I'd rather keep things simple.

@mmcdon20
Copy link

Declare something which is only a namespace:

namespace Name {
  // members
}

Is upper camel case the preferred casing for namespaces?

The reason I ask is that I see import/as as a type of namespacing:

import 'dart:math' as math;

And the convention for import/as is to use lowercase with underscores.

Would it be more consistent if namespace declarations followed the same convention?

@nate-thegrate
Copy link

I felt good about closing #2272 after we got new class modifiers in Dart 3.0. After reading through this issue I still feel mostly the same way, but I do think it'd be great to have locally-scoped typedefs and the ability to show/hide static fields in an import or export.


As far as keeping things simple, I believe a lot of the functionality described here could be achieved via the proposal from #2254.

On multiple occasions I've tried sticking to a pattern of import 'foo.dart' as foo; but then gave up since it's much more convenient to just let automatic imports handle things. But if the as foo were defined in the export, maybe we could have the best of both worlds!

@FMorschel
Copy link

@nate-thegrate to note:

I've fixed dart-lang/sdk#56608 (and a bunch of related issues about imports that you can probably follow from the links there) recently. One of them adds the ability to write foo.identifier and it'll suggest to import the library with the foo prefix dart-lang/sdk#55863. Hopefully if you ever try to do something like that again, it'll be easier now.

@FMorschel
Copy link

FMorschel commented May 2, 2025

I was thinking a bit more about this issue yesterday while fixing some assists/fixes. On those files, we use a lot of unnamed extensions to make some things more readable.

I stumbled upon a case where, in processing the fix, I got to a DartType value, and I just wanted to test if it was assignable to Iterable. I had to use typeProvider and typeSystem getters from ResolvedCorrectionProducer.

It would be much nicer if I could write an extension for DartType inside that class and be able to access those getters (referenced CL https://dart-review.googlesource.com/c/sdk/+/425540):

class AddAwait extends ResolvedCorrectionProducer {

  @override
  Future<void> compute(ChangeBuilder builder) async {
    // Some code ...
    if (!type.isAssignableToIterable) {
      return;
    }
    // More code ...
  }

  extension on DartType {
    bool get isAssignableToIterable {
      return typeSystem.isAssignableTo(this, typeProvider.iterableDynamicType);
    }
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems
Projects
None yet
Development

No branches or pull requests

5 participants