Skip to content
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

Analyzer regression around diagnostics when imported files are not generated #42832

Open
bwilkerson opened this issue Jul 24, 2020 · 25 comments
Open
Assignees
Labels
analyzer-ux area-analyzer Use area-analyzer for Dart analyzer issues, including the analysis server and code completion. P3 A lower priority bug or feature request type-enhancement A request for a change that isn't a bug

Comments

@bwilkerson
Copy link
Member

The analyzer used to notice when an import, export or part directive referenced a generate file that does not exist and silenced follow-on diagnostics. That behavior appears to have been broken. For example, a file containing the following:

import 'doesNotExist.g.dart';

void f(A a) {
  var b = A.b;
}

Now produces 3 diagnostics when it ought to produce a single "generated file does not exist" diagnostic.

This has serious implications for internal users.

@bwilkerson bwilkerson added area-analyzer Use area-analyzer for Dart analyzer issues, including the analysis server and code completion. type-bug Incorrect behavior (everything from a crash to more subtle misbehavior) labels Jul 24, 2020
@bwilkerson
Copy link
Member Author

@scheglov

@scheglov
Copy link
Contributor

Can you point me at a document that specifies the required behavior?

What I see implemented, and what I vaguely remember hearing when this feature was discussed, is that we ignore errors reported on identifiers that are either (1) imported with a prefix; (2) imported without a prefix but with an explicit show combinator. In both cases the library must be synthetic. meaning that its file is missing.

The example provided in the first comment does not satisfy these conditions, so we report multiple errors.
But for the variants we report only one.

import 'doesNotExist.g.dart' show A;

void f(A a) {
  A.b;
}

or

import 'doesNotExist.g.dart' as p;

void f(p.A a) {
  p.A.b;
}

In both cases we get CompileTimeErrorCode.URI_HAS_NOT_BEEN_GENERATED [7, 21, Target of URI hasn't been generated: 'doesNotExist.g.dart'.].

We have a disjoined check for reporting a special error for a missing file when it looks like it might be generated.
But I don't remember changes there, it has always been private, since it was added in 2017.

@bwilkerson
Copy link
Member Author

Can you point me at a document that specifies the required behavior?

Unfortunately, no. I don't believe that we documented the desired behavior.

... we ignore errors reported on identifiers that are either (1) imported with a prefix; (2) imported without a prefix but with an explicit show combinator.

I don't remember those restrictions, but it's quite possible that we decided to do that in order to reduce the number of false negatives.

If that's the case, there is no regression. But I think we should consider broadening the support because using show combinators and prefixes won't work with part directives, which are what's used with most of the code generators. I suspect that users would even be happy having this loosening apply only to part directives at this point.

@scheglov scheglov added type-enhancement A request for a change that isn't a bug and removed type-bug Incorrect behavior (everything from a crash to more subtle misbehavior) labels Jul 26, 2020
@scheglov
Copy link
Contributor

OK, then this is not a bug, but an enhancement.

Without additional information we cannot distinguish an identifier that should come from an imported library, and identifier that should come from a part. But I can imagine that there might be a correspondence between a generated part name, and the pattern of names of generated entities in the part.

Do you have examples of such parts and generated identifiers?
Maybe google3 targets which I could look at?

@scheglov scheglov added the needs-info We need additional information from the issue author (auto-closed after 14 days if no response) label Jul 26, 2020
@scheglov scheglov self-assigned this Jul 26, 2020
@bwilkerson
Copy link
Member Author

@jwren

@no-response no-response bot removed the needs-info We need additional information from the issue author (auto-closed after 14 days if no response) label Jul 27, 2020
@greglittlefield-wf
Copy link
Contributor

Related: #42977 (see note at the bottom of the issue)


Do you have examples of such parts and generated identifiers?

I have some examples here for over_react, if they're worth considering 🙂:

(There are other examples under example/builder/src) as well.

(Note that in this repo the generated files use a build-to-source builder and are checked in, but all consumer repos use build-to-cache.)

To summarize:

  • All of our generated members have names that start with either $ or a series of _ and $
  • Names in the generated file are derived from prefixing and/or postfixing names in the source file, but a single source member may result in multiple generated members
    • For example: BasicProps results in $BasicProps, _$BasicProps, _$$BasicProps$PlainMap, etc. being generated
    • However, we only support referencing a few of those members in the source file

@devoncarew
Copy link
Member

devoncarew commented Oct 6, 2020

Hey @greglittlefield-wf - to follow up on your last comment (and, cc'ing in @robbecker-wf):

We're looking at expanding the file name patterns for what we consider probably generated code, and, how we then identify the following undefined symbols in that file, to know which missing symbol errors to ignore.

We currently support two patterns for this - import 'doesNotExist.g.dart' show A; and import 'doesNotExist.g.dart' as p;. The nice things about these imports is that they encode the information in them to let us know which errors to suppress (the shown symbols or the ones referenced by the given prefix).

For the imports of the form part 'part_name.g.dart';, we don't have an easy way to know which following errors to suppress.

One possible solution to this would be to use part 'part_name.g.dart'; to identify probably generated code, and _$GeneratedSymbol to then suppress following issues. From @kevmoo, this matches the codegen pattern used by several libraries. Would this generally work for your use case? Note that this would catch _$$Symbol names but not $Symbol names.

One follow-on concern we have here is that this could unintentionally catch user code which is not using codegen.

@alanknight-wk
Copy link

alanknight-wk commented Oct 6, 2020

If you allowed user to specify the form of generated symbols, would that help with the concern for false negatives.
e.g.
@ExpectGeneratedNamesToStartWith('_$')

@bwilkerson
Copy link
Member Author

Yes, that would help.

It would be better if users didn't need to repeat that pattern in every library with a generated part, so we could consider adding a list of generatedNames patterns to the analysis options file. Even better would be some way for generators to publish the patterns that they produce so that analyzer could compute the list by looking at the dependencies, but maybe that would be too much magic.

@greglittlefield-wf
Copy link
Contributor

@devoncarew Yes, I believe that would work for our use-case!

We currently have a subset of boilerplate that references generated $Symbol names (example reference and generated code), but we could definitely update those to be _$Symbol instead.

@scheglov
Copy link
Contributor

scheglov commented Oct 9, 2020

@alanknight-wk
Copy link

If it only applies when there's a missing generated file then it will still cause errors while people are editing a file that uses a code generator.

@bwilkerson
Copy link
Member Author

Do you mean: in the case where the file was generated but the generated file is not out of date?

@alanknight-wk
Copy link

Is not up to date, yes.

@bwilkerson
Copy link
Member Author

How important is it to support that use case (not reporting diagnostics when a generated file is out of date)?

@alanknight-wk
Copy link

Hard to say. It depends a lot on how much the code generator is doing. If, as soon as you touch anything in the file, it fills up with false errors, that seems bad - makes analysis useless for that file. But I think a lot of generators try to minimize how much of the original file is affected. Is it difficult to detect when the generated file is older than the current file and treat that the same as if it was missing?

@natebosch
Copy link
Member

Hard to say. It depends a lot on how much the code generator is doing. If, as soon as you touch anything in the file, it fills up with false errors, that seems bad - makes analysis useless for that file.

If the content was this unstable I would expect that the // ignore also go stale on a change.

The only new errors that should surface here would be, I think, the same places you'd need to add a new // ignore using the old strategy. It would be good to get some real world experience trying this approach and see how it feels in practice.

@bwilkerson
Copy link
Member Author

Is it difficult to detect when the generated file is older than the current file and treat that the same as if it was missing?

No, but that means that any change to the current file, such as adding a space, would block analysis support until the generated file is regenerated. I don't think that's a good UX either.

@greglittlefield-wf
Copy link
Contributor

greglittlefield-wf commented Oct 9, 2020

At least for the over_react use-case, the majority of the time, the code that references generated members isn't often edited after it's introduced. Usually, it's one UI component per file of the same name. For example, this isn't very common:

part 'foo.over_react.g.dart';

-UiFactory<FooProps> Foo = _$Foo;
+UiFactory<BarProps> Bar = _$Bar;

...and in those cases the solution from that CL would work well.

However, there's a newer case that isn't very common yet, but I suspect could become more common, where you have multiple, smaller UI components per file. This means new code can be added to existing files that references new generated members. For example:

part 'foo.over_react.g.dart';

UiFactory<FooProps> Foo = _$Foo;

...

+UiFactory<BarProps> Bar = _$Bar;

...and that would be an issue, both when users are adding that new code themselves, or when switching to branches that contain added code.


As a middle ground between only ignoring errors when the generated files are missing vs. only erroring when the source file hasn't been modified, could we update the error messages for undefined members that match that _$ pattern to suggest rerunning the code generator?

Since the situation is less common, my main concern would be more around users being confused about how to resolve the issue, as opposed to being around them having to rerun a build to resolve it.

For example, instead of just saying

error: Undefined name '_$Foo'. Try correcting the name to one that is defined, or defining that name.

...it also said something like:

If the name is generated, try checking that the generated code is up to date, and that the name is expected to be generated based on the source code and generator configuration.

@greglittlefield-wf
Copy link
Contributor

greglittlefield-wf commented Oct 9, 2020

@scheglov @bwilkerson I tried out that CL locally and it works great except for one thing: I'm seeing that errors related to disabled implicit casts aren't ignored.

For example:

part 'foo.g.dart';

class Foo {}
methodCall(Foo foo) {}

// error: A value of type 'dynamic' can't be assigned to a variable of type 'Foo'. (invalid_assignment)
Foo foo = _$foo;

// error: The argument type 'dynamic' can't be assigned to the parameter type 'Foo'. (argument_type_not_assignable)
var result = methodCall(_$foo);

Are there plans to silence invalid_assignment/argument_type_not_assignable as well? We could work around it with explicit casts, but it would be really nice to not have to.

@scheglov
Copy link
Contributor

Well, that's a complication.
Any unresolved identifier gets the dynamic type.
And later we don't know if something was declared as dynamic, or was given dynamic because of a previous error.
This is not different from Foo foo = resolved.unresolved();.

dart-bot pushed a commit that referenced this issue Oct 14, 2020
Change-Id: I5a103cd362318b4db292b5f41da9e56a60f6431d
Bug: #42832
Bug: #42977
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/166820
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
@bwilkerson
Copy link
Member Author

At one point we had an "undefined" type that was distinct from dynamic so that we could distinguish between these two cases. I seem to remember that we removed it because it caused complications in the implementation, but I don't remember what kind of complications specifically.

@greglittlefield-wf
Copy link
Contributor

greglittlefield-wf commented Oct 15, 2020

So, I assume re-implementing a distinction between the two cases is out of the question?

That aside, just to make sure I have a correct understanding of what the path forward for us currently looks like...

We'd be going from what we have now:

UiFactory<FooProps> Foo = _$Foo; // ignore: undefined_identifier, invalid_assignment

...to utilizing the latest implementation added in that commit and either:

// A: Adding an implicit cast.
UiFactory<FooProps> Foo = _$Foo as UiFactory<FooProps>;

// B: Adding an implicit cast and removing the LHS typing to avoid
//    having to repeat the type.
final Foo = _$Foo as UiFactory<FooProps>;

// C: Using a helper method that performs a cast without having to repeat the type.
T cast<T>(dynamic value) => value as T;
UiFactory<FooProps> Foo = cast(_$Foo);
old incorrect option B for posterity
// B: Adding an implicit cast and removing the LHS typing to avoid
//    having to repeating the type.
//    But, this would conflict with the type_annotate_public_apis Effective Dart lint,
//    so we probably wouldn't pick it.
var Foo = _$Foo as UiFactory<FooProps>;

(It might be worth noting that having the cast isn't ideal because it prevents static type-checking from occurring once the generated field is present.)

While having to perform that explicit cast isn't ideal, if we can eventually replace them with external variables like what was proposed in this comment #42977 (comment), then having to live with this until then seems acceptable.

And sorry for bringing up the invalid_assignment issue last-minute! We haven't turned off implicit casts in most of our repos, so it wasn't top of mind 😓 .

@kevmoo
Copy link
Member

kevmoo commented Oct 15, 2020 via email

@greglittlefield-wf
Copy link
Contributor

Oh you're right: making it final gets rid of the type_annotate_public_apis lint! Nice!! I'll update that example

-var Foo = _$Foo as UiFactory<FooProps>;
+final Foo = _$Foo as UiFactory<FooProps>;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
analyzer-ux area-analyzer Use area-analyzer for Dart analyzer issues, including the analysis server and code completion. P3 A lower priority bug or feature request type-enhancement A request for a change that isn't a bug
Projects
None yet
Development

No branches or pull requests

8 participants