dart-lang / sdk

The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
https://dart.dev
BSD 3-Clause "New" or "Revised" License
10.27k stars 1.58k forks source link

Analyzer allows accessing statics/members on type aliases alising a type variable. #52812

Open lrhn opened 1 year ago

lrhn commented 1 year ago

Dart allows accessing static members and constructors of classes (class/mixin/enum declarations) through type aliases, but only if the type alias doesn "expand to a type variable" (and then only if it explicitly denotes a class.)

A type alias expands to a type variable if its RHS is a type variable, or it's Q<TypeArgsOpt> where Q is a (possibly qualified) identifier denoting a type alias which expands to a type variable. (We don't care what the type arguments are, we just follow the name at the head of the type until a non-type-alias is found.)

So a type alias explicitly denotes a non-alias declaration C if its RHS is = Q<TypeArgsOpt>; where Q is an identifier or qualified identifier directly denoting C, or transitively if Q denotes another type alias declaration which then explicitly denotes C.

Further, accessing statics on instantiated type expressions (Q<TypeArgs>) is always an error. If Q<TypeArgs> is a type expression, then the only continuations are constructor tear-offs or invocations, (args), .new(args), .name(args), .new, or .name. Any other follow-up selector is a compile-time error.

Which means you can't do typedef A<T> = T; A<int>.parse("1"); for two reasons: parse is not a constructor, so it can't be invoked on an instantiated type expression at all, and A doesn't denote a class.

Example:

void main() {
  A<C>.named;
  A<C>.stat;
  A<C>.inst;

  C2<int>.stat;
}

extension on Type {
  get inst => 42;
}

class C {
  C();
  C.named();
  static get stat => 0;
}
typedef A<T> = T;

typedef C2<T> = C;

The front end gives the following errors (from dartpad.dev).:

lib/main.dart:3:3:
Error: Can't use a typedef denoting a type variable as a constructor, nor for a static member access.
  A<C>.named;
  ^
lib/main.dart:19:11:
Info: This is the type variable ultimately denoted.
typedef A<T> = T;
          ^
lib/main.dart:4:3:
Error: Can't use a typedef denoting a type variable as a constructor, nor for a static member access.
  A<C>.stat;
  ^
lib/main.dart:19:11:
Info: This is the type variable ultimately denoted.
typedef A<T> = T;
          ^
lib/main.dart:5:3:
Error: Can't use a typedef denoting a type variable as a constructor, nor for a static member access.
  A<C>.inst;
  ^
lib/main.dart:19:11:
Info: This is the type variable ultimately denoted.
typedef A<T> = T;
          ^
lib/main.dart:7:11:
Error: Cannot access static member on an instantiated generic class.
  C2<int>.stat;
          ^^^^
lib/main.dart:3:8:
Error: The getter 'named' isn't defined for the class 'Type'.
 - 'Type' is from 'dart:core'.
  A<C>.named;
       ^^^^^
lib/main.dart:4:8:
Error: The getter 'stat' isn't defined for the class 'Type'.
 - 'Type' is from 'dart:core'.
  A<C>.stat;
       ^^^^
Error: Compilation failed.

which is one error per place the term A<C> is used as receiver for a member access, and one where C2<int> is used. (Plus two spurious errors where it seems to try doing the access on a Type object instead, which shouldn't happen.)

The analyzer gives only one error:

error
line 7 • The static member 'stat' can't be accessed on a class instantiation.
Try removing the type arguments from the class name, or changing the member name to the name of a constructor.
info
line 11 • The declaration 'inst' isn't referenced.
Try removing the declaration of 'inst'.

(and one info, saying that the extension method inst isn't used, which is doubly weird).

Something is going wrong with errors about accessing members through type aliases which expands to a type variable. The analyzer correctly recognizes that C2<int>.stat is not allowed, but not that A<C>.stat is not allowed for the same reason, and for another reason too.

eernstg commented 1 year ago

Let's try to see which rules we already have in the language specification. This one is clearly relevant to syntax like A<C>.id, because A<C> is a type literal in current Dart:

Let id be an identifier; a static property extraction i is an expression of the form C.id, where C is a type literal or C denotes an extension. A compile-time error occurs unless C denotes a class, a mixin, or an extension that declares a static member named m ...

(There's a typo here: it should say "named id"; #2605 corrects that.)

We should adjust this rule to cover the case where C.id or C<T1..Ts>.id is a constructor tear-off (that's OK, too).

We should also adjust this such that it explicitly covers the case where the type literal is a parameterized type (that is, it has the form C<T1 .. Ts> for some C and T1 .. Ts): This is an error for a member marked static, but OK (subject to further checks, of course) for a constructor tear-off.

Finally, we should generalize the rule that makes it an error to use a type alias that denotes a type variable as a class in specific situations, cf. this location in the specification. This rule should be extended to say that a constructor tear-off is such a location: Assume that F is a type alias that denotes a type variable. Then F.new, F<T1 .. Ts>.new use F as a class when the denoted class C declares a constructor named C. Moreover, F.name and F<T1..Ts>.name use the type alias F as a class when the denoted class C declares a constructor named C.name.

(Of course, when the denoted class does not declare a constructor with the given name, nor a member with the modifier static, it is an error as before. We still do not look into Type instance members or extension instance members in order to allow this property extraction, we insist that the type literal is used to perform a "static lookup".)

This would make A<C>.stat an error: A is a type alias that denotes a type variable, and hence it is an error to use A<C> as a class, and C.stat is indeed a member of C with the modifier static.

A<C>.named is an error for nearly the same reason (based on a constructor tear-off rather than a member marked static). And A<C>.new would be an error for the same reason.

Finally, A<C>.inst is an error because A<C> is a type literal, and this implies that we never evaluate the type literal to an object of type Type and then invoke an instance member of Type on it.

I think this clarifies the situation: We need to update the specification, but we already know that, and the updates could be as outlined above.

@dart-lang/language-team, do you think we need to have language repo discussion issues about this, or can we proceed to update the language specification as hinted above?

If we agree on (essentially) the rules mentioned above then we should adjust the tools to report an error in all cases as mentioned above.

bwilkerson commented 1 year ago

@scheglov