Skip to content

Extend private named parameters to apply to positional parameters too. #4486

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
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
177 changes: 145 additions & 32 deletions working/2509-private-named-parameters/feature-specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,12 @@ void pricyHammer() => Hammer(price: 200);

## Static semantics

Prior to this proposal, a parameter only had one name which was used everywhere
the parameter can be referred to. With this proposal, "parameter name" can be
ambiguous, so we introduce some terminology.

### Public and private names

An identifier is a **private name** if it starts with an underscore (`_`),
otherwise it's a **public name**.

Expand All @@ -385,33 +391,60 @@ underscore does not leave something which is is a valid identifier *(as in `_`
or `_2x`)* or leaves another private name *(as in `__x`)*, then the private name
has no corresponding public name.

### Declared and access names

* A parameter's **declared name** is the original identifier used to introduce
the formal parameter. It's the name of the local variable used to access the
argument value bound to a parameter inside the body of a function or
constructor initializer list.

If the parameter initializes or declares an instance variable, the declared
name determines the name of the corresponding instance variable.

* A parameter's **access name** is the name that allows users of surrounding
Copy link
Member

@lrhn lrhn Aug 7, 2025

Choose a reason for hiding this comment

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

Not sure "access name" works for me. It feels like the name you use to read it, not to set (pass) it.

I'd just use "parameter name", the name of the parameter in the function type of the function/constructor.


A parameter declaration has a declared name, which is the syntactic identifier used to introduce the formal parameter. (Or it might not, if it's declared identifier is _.)
Examples of parameters where name is the declared name: name, int name, this.name, final int name, super.name, and (for completeness, not encouragement) int name().

The declaration introduces a local parameter variable into the parameter scope (normal parameters only) and the initializer list scope (all kinds of parameter), bound to the argument value passed to that parameter.
The parameter variable's name is the same as the declared name.
Example where x is in the initializer list scope and deltaY is in the parameter
scope:

class Point {
  final int x, y;
  Point(this.x, this.y);
  Point._offSetFromDiagonal(this.x, int deltaY) : y = x + deltaY {
    if (deltaY == 0) log("Another diagonal point!: ($x, $x)");
  }
}

The parameter declaration also introduces a function parameter to the constructors function signature. This name occurs in the function signature, but not in any scope, and it is the name used to refer to the parameter from outside of the constructor declaration. For named parameters, it is the name used to pass an argument at call sites. For positional parameters, the name is never used in code to denote the parameter outside of the function. Both can be referenced indirectly by tools that refer to source names, like DartDoc, in error messages, or while debugging.
Example with references to the names of value and negated:

extension type const N._(int value) {
  /// Creates [N] with [value] as [N.value], negated if [negated] is true.
  const N(int value, {bool negated = false}) 
    : assert(!(value == 0 && negated), 
          "The 'value' must not be zero when 'negated' is true"),
      value = negated ? -value : value;
}

For non-constructor parameters, there is also a lint to ensure that an overriding method has the same positional parameter names as the
function it overrides.

... and the parameter name of field parameters and initializing formals
is now not always the declared name ...


(Now I waxed didacticly again. I really need to learn to stop writing what I'm thinking. Or while I'm thinking. And I'm doing it again!)

Point is: "access name" feels like the opposite of "name used to pass an argument.

I like "parameter name" because that's what we already call the name of the parameter of a function from the outside, which is what this name is.

Use what you want of the above, or ignore it. I tend towards over-explaining things.

Today we use "parameter" and occasionally "parameter name" for internal local variables today, because we haven't made a distinction. Now we should make that distinction, so I suggest "parameter" and "parameter variable", and "parameter name" and "parameter variable name". And for field parameters also a "field name" of the instance variable declared by the field parameter.

Copy link
Member Author

Choose a reason for hiding this comment

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

Not sure "access name" works for me. It feels like the name you use to read it, not to set (pass) it.

Me either, but it was the best I could think of. I thought about it some more and came up with "visible name". I think that works well. It's not exactly the same as "public" (so we can distinguish a public identifier from a visible parameter), but it conveys pretty much the same thing.

(Now I waxed didacticly again. I really need to learn to stop writing what I'm thinking. Or while I'm thinking. And I'm doing it again!)

<3 :D

declaration to work with the parameter. It's the name written at a callsite
to pass a named argument to the function or constructor.

It's the name that should be shown for the parameter in generated
documentation from tools like [dartdoc][]. When other documentation wants to
refer to the parameter using the `[squareBracket]` identifier syntax, that
doc comment should refer to it by its access name.

If a Dart implementation refers to the parameter in any automatically
generated code or strings, those should use its access name. For example,
implicit implementations of `toString()`, runtime error messages, stack
traces, etc.

In short, for all *users* of some code, it should appear as if the parameter
only has its access name.

[dartdoc]: https://dart.dev/tools/dart-doc

Unless otherwise specified, the access name of a parameter is its declared name.
The exceptions are:

### Private named parameters

Given a named initializing formal or field parameter (for a primary constructor)
with private name *p* in constructor C:

* If *p* has no corresponding public name *n*, then compile-time error. *You
can't use a private name for a named parameter unless there is a valid
public name that could be used at the call site.*

* If any other parameter in C has declared name *p* or *n*, then
compile-time error. *If removing the `_` leads to a collision with
another parameter, then there is a conflict.*

If there is no error then:

* The parameter name of the parameter in the constructor is the public name
*n*. This means that the parameter has a public name in the constructor's
function signature, and arguments for this parameter are given using the
public name. All uses of the constructor, outside of its own code, see only
the public name.
* If any other parameter in C has declared name *n*, then compile-time error.
*If removing the `_` leads to a collision with another parameter, then there
is a conflict. It is of course already an error if any other parameter has
declared name _p_. That's a normal parameter name collision error.*

* The local variable introduced by the parameter, accessible only in the
initializer list, still has the private name *p*. *Inside the body of the
constructor, uses of _p_ refer to the instance variable, not the parameter.*

* The instance variable initialized by the parameter (and declared by it, if
the parameter is a field parameter), has the private name *p*.
```dart
class C {
C(int _a, {this._a}); // Already an error for parameter names to collide.
C(int a, {this._a}); // New error because the public name collides.
}
```

* Else the field parameter induces an instance field with name *p*.
* Otherwise, the access name of the parameter is *n*.

*For example:*

Expand All @@ -431,25 +464,85 @@ main() {
}
```

*Note that the proposal only applies named parameters and only to ones which are
*Note that this section only applies to named parameters that are also
initializing formals or field parameters. A named parameter can only have a
private name in a context where it is _useful_ to do so because it corresponds
to a private instance field. For all other named parameters it is still a
compile-time error to have a private name.*

### Private positional parameters

Dart already allows a positional parameter to have a private name. This is
useful for a positional constructor parameter that is an initializing formal or
a declaring parameter for a private instance variable. For other parameters,
it's non-idiomatic but harmless to give them private names. *(Dart also allows
local variables to have private names, which is unusual but harmless. A lint
suggests that users don't do this.)*

However, the user experience of the language is more than just the semantics.
Generated documentation, code navigation in doc comment references, and runtime
error messages all reflect a function or constructor's parameters back to the
user.

In all of those contexts, we want it to be an implementation detail that a
positional parameter happens to have a private name. To that end:

Given a positional parameter with private name *p* in formal parameter list L:
Copy link
Member

Choose a reason for hiding this comment

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

I'd still only do this for initializing formals and field parameters, where there is a reason to have a private name. If you ask for it otherwise, you're on your own.

Copy link
Member

Choose a reason for hiding this comment

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

Why make things egregiously inconsistent? If we're going to do this (I'm not convinced) we can at least be consistent.

Copy link
Member

Choose a reason for hiding this comment

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

Maybe mention that _ is not a private name. (Because it's not a name at all, the parameter is unnamed.)


* If *p* has corresponding public name *n* and no other parameter in L has
Copy link
Member

@lrhn lrhn Aug 7, 2025

Choose a reason for hiding this comment

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

And maybe also make it explicit that it's a compile-time error if any other parameter has declared name p.

(But it is true that every named parameter introduces a binding into the initializer list scope, for its private name, so we will get a "same name twice in one scope" error.)

declared name *n*, then the access name of the parameter is *n*.
Copy link
Member

Choose a reason for hiding this comment

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

... , otherwise it's p.

And for all other parameter declarations, the parameter name is the declared name.

Can't fall back on defaults without an "otherwise", because we don't want to hit the default definition at all, which would says that the access name is the declared name. If we can hit that by simple fallthrough, then we'll also hit it from the clause that says the access name is n, and then it has two access names. Exhaust all the cases!

So, in total:

  • If a named parameter is an initializing formal or field parameter with a private declared name p which has a corresponding public name n, then:
    • It's a compile time error if any other parameter of the same constructor has declared name p.
    • The access name of the parameter is n.
  • If a positional parameter has a private declared name p with corresponding public name n, and no other parameter of the same function or constructor has declared name p or n,
    • then the access name of the parameter is n.
  • Otherwise the access name of the parameter is p.
  • It's a compile-time error if two parameters of the same function or constructor have the same access name.

Is that about right?


Since a positional parameter can't be passed using a named argument, this
doesn't affect the language semantics. It does mean that generated docs, error
messages, inferred super parameter names, etc. should all use the public name
Copy link
Member

Choose a reason for hiding this comment

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

The "inferred super parameter names" are the ones of anonymous mixin applications?
Maybe say that: "of mixin application classes". They're probably not specified as super-parameters, just as "forwarding constructors", so pedantically: "Names of parameters of forwarding constructors of mixin application classes".

Or maybe drop it, it's not an exhaustive list, and it's hard to explain.

of the parameter. For example:*

```dart
const int foo = 0;

class C {
int _foo;

/// Uses [foo] to set [_foo].
C(this._foo);
}
```

Here, the generated documentation for C should show the constructor signature
like `C(int foo)`. If a user clicks the `[foo]` reference in the doc comment
and navigates to its definition, their IDE should take them to the constructor
parameter `_foo` and not the unrelated top level constant `foo`.

In short, a class author should never feel that can't use a private name for
a positional initializing formal or declaring parameter because doing so might
degrade the user experience of anyone working with the class or reveal the
implementation detail that the parameter happens to initialize a private field.

*Unlike with named parameters, this section applies to all positional
parameters, regardless of whether they are initializing formals, declaring
parameters, or even constructor parameters at all. This is because the language
already allows using private names in all positional parameters, and we want a
consistent user experience for all of them.*
Copy link
Member

Choose a reason for hiding this comment

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

Maybe it's a good idea. Maybe it's inconsistent that it doesn't work for other kinds of named parameters too. I could see myself wanting to write bar({int _foo = 0}) { ... } to not shadow a surrounding foo, while still getting foo as parameter name.

I think it's an easier story to tell if it only works for initializing formals and field parameters, parameters whose name also refers to something else, and that other thing might want to be private. For everything else, if you choose a private name, it's because you wanted one, and you could just use a public name.


## Runtime semantics

There are no runtime semantics for this feature. It's purely a compile-time
renaming.

## Compatibility

This proposal takes code that it is currently a compile-time error (a private
named parameter) and makes it valid in some circumstances (when the named
parameter is an initializing formal or field parameter). Since it simply expands
the set of valid programs, it is backwards compatible. Even so, it should be
language versioned so that users don't inadvertently use this feature while
their program allows being run on older pre-feature SDKs.
For named parameters, this proposal takes code that it is currently a
compile-time error (a private name) and makes it valid in some circumstances
(when the named parameter is an initializing formal or field parameter). Since
it simply expands the set of valid programs, it is backwards compatible.

For positional parameters with private names, it falls back to continuing to use
the private name as the access name in the rare case that there is a collision.
That avoids breaking existing uses of private names in parameter lists.

Even though non-breaking, this feature should be language versioned so that
users don't inadvertently use this feature while their program allows being run
on older pre-feature SDKs.

## Tooling

Expand All @@ -459,18 +552,33 @@ normative*, but is merely suggestions and ideas for the implementation teams.
They may wish to implement all, some, or none of this, and will likely have
further ideas for additional warnings, lints, and quick fixes.

### Error messages

Compile errors and runtime exceptions (from things like invalid dynamic calls)
may sometimes show a function's signature to the user or otherwise refer to a
formal parameter. When tools generate these error strings, they should use the
access name of each parameter if the error relates to a *use* of the parameter
list. *For example, an error related to calling a function with an incorrect
parameter type.*

Errors that refer to accessing the parameter from within the function or
Copy link
Member

Choose a reason for hiding this comment

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

... accessing the parameter variable ...

(to make a distinction between the parameter and its introduced variable.)

constructor should use its declared name. *For example, an error related to
trying to assign to a formal parameter marked `final`, or an initializing formal
with no corresponding instance variable.*

### API documentation generation

Authors documenting an API that uses this feature should refer to the
constructor parameter by its public name since that's what users will pass.
Likewise, doc generators like [`dart doc`][dartdoc] should document the
constructor's parameter with its public name. The fact that the parameter
initializes or declares a private field is an implementation detail of the
class. What a user of the class cares about is the corresponding public name for
the constructor parameter.
If they don't already, doc generators like [`dart doc`][dartdoc] should be
updated to always use a parameter's access name when referring to the parameter.

[dartdoc]: https://dart.dev/tools/dart-doc

### In-editor documentation

IDEs will often show inline generated documentation or signatures when hovering
over declarations or callsites. When an editor or IDE synthesizes a signature
for a formal parameter list, it should use the access name of every parameter.

The language already allows a *positional* parameter to have a private name
since doing so has no effect on call sites. Doc generators are encouraged to
also show the public name for those parameters in generated docs too. The fact
Expand Down Expand Up @@ -524,6 +632,11 @@ can help users learn the feature.

## Changelog

### 0.3

- Apply the same renaming to private positional parameters too (for doc
generators, error messages, etc.) (#4479).

### 0.2

- Add section about concerns for learnability and mitigations.
Expand Down