Skip to content

Conversation

@LongCatIsLooong
Copy link
Contributor

@LongCatIsLooong LongCatIsLooong commented Jul 6, 2023

Also fixes #103917 (but resolving the entire framework takes about 50 seconds while the original implementation only takes a few seconds).

Introduces an _debugAssert annotation that verifies the annotated getters/setters/constructors/functions/methods in the framework are only directly or indirectly called inside asserts (checking kDebugMode will be nice but there doesn't seem to be an easy way of doing this kind of control flow analysis).

In the rare case where a _debugAssert annotated method gets overridden, the override is also considered debug-only. The analyzing code reports an error if a method override has @_debugAssert but the super implementation isn't debug only (unless the super implementation isn't defined in the framework).
Synthesized default constructors are considered debug-only if it's superclass's default constructor is debug-only.

It would be nice to have a real lint so the feedback is instantaneous and works outside of the framework codebase. Would be even nicer if it accounts for kDebugMode/kProfileMode in addition to --enable-asserts.

TODOs after this:

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the Flutter Style Guide, including Features we expect every widget to implement.
  • I signed the CLA.
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is test-exempt.
  • All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@github-actions github-actions bot added a: text input Entering text in a text field or keyboard related problems c: contributor-productivity Team-specific productivity, code health, technical debt. framework flutter/packages/flutter repository. See also f: labels. a: animation Animation APIs f: material design flutter/packages/flutter/material repository. f: scrolling Viewports, list views, slivers, etc. f: cupertino flutter/packages/flutter/cupertino repository labels Jul 6, 2023
@LongCatIsLooong LongCatIsLooong force-pushed the flutter-internal-lints branch 4 times, most recently from 69d82a3 to e61e97f Compare July 6, 2023 22:37
@LongCatIsLooong LongCatIsLooong force-pushed the flutter-internal-lints branch from e61e97f to 1cc26e6 Compare July 6, 2023 22:39
@LongCatIsLooong LongCatIsLooong force-pushed the flutter-internal-lints branch 3 times, most recently from cd98289 to ab6ce8d Compare July 7, 2023 01:31
@LongCatIsLooong
Copy link
Contributor Author

Hi @pq and @srawlins do you mind reviewing this?

@Hixie
Copy link
Contributor

Hixie commented Jul 11, 2023

Should we just make anything called debugFoo subject to this automatically, rather than requiring an annotation?


import '../utils.dart';

/// Analyzes the given `workingDirectory` with the given set of [AnalyzeRule]s.
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't work for any workingDirectory, though. The working directory must contain the flutter package at package/flutter/lib. Typically we call this directory the flutter root, so I would just call this parameter flutterRoot (or actually make analyzeDirectoryWithRules work on any directory).

Comment on lines 62 to 63
/// Reports all violations in the resolved compilation units [applyTo] was
/// called all.
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand this sentence...

/// The implementation typically calls [foundErrors] to report violations.
void reportViolations(String workingDirectory);
/// Applies this rule to the given resolved compilation unit (which is
/// typically a file).
Copy link
Member

Choose a reason for hiding this comment

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

document what one should do with failures (i.e. this shouldn't throw or anything and just record the failures internally until reportViolations is called)

void reportViolations(String workingDirectory);
/// Applies this rule to the given resolved compilation unit (which is
/// typically a file).
void applyTo(ResolvedUnitResult unit);
Copy link
Member

Choose a reason for hiding this comment

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

nit: I would switch the order of these methods around, logically, you need to call applyTo first and then worry about how to get the failures back out.

import '../utils.dart';
import 'analyze.dart';

/// An [AnalyzeRule] that verifies annotated getters, setters, constructors,
Copy link
Member

Choose a reason for hiding this comment

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

document: annotated with what?

@LongCatIsLooong LongCatIsLooong marked this pull request as draft July 11, 2023 17:44
@LongCatIsLooong
Copy link
Contributor Author

LongCatIsLooong commented Jul 11, 2023

Should we just make anything called debugFoo subject to this automatically, rather than requiring an annotation?

I originally made that an annotation because I wanted to mark an entire library debug-only (primarily for diagnostics.dart but it's also used in profile mode so that didn't work). Also according to @goderbauer we use _debugX in profile mode too .

@LongCatIsLooong LongCatIsLooong force-pushed the flutter-internal-lints branch from 4dd7474 to b55969e Compare July 11, 2023 19:53
@LongCatIsLooong LongCatIsLooong marked this pull request as ready for review July 12, 2023 22:02
Copy link
Contributor

@srawlins srawlins left a comment

Choose a reason for hiding this comment

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

I've made it halfway through debug_assert.dart, but have to pause for a while, so I'm sending out my comments so far.

/// outside of an assert.
///
/// The annotation can also be added to classes, mixins, extensions, or
/// libraries, in which case all non-synthetic memebers defined within that
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: 'members'


export 'print.dart' show DebugPrintCallback;

/// An annotation that prevents annotated getters, setters, constructors,
Copy link
Contributor

Choose a reason for hiding this comment

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

and operators?

/// functions, methods and fields to be called/accessed directly or indirectly
/// outside of an assert.
///
/// The annotation can also be added to classes, mixins, extensions, or
Copy link
Contributor

Choose a reason for hiding this comment

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

Just to have great documentation while this is all loaded into our brains: what about inheritence?

  1. If B extends A and A.m is annotated with @debugAssert, can B.m be called outside an assert?
  2. If B extends A and A is annotated with @debugAssert, can any of B's members be called outside of an assert?

(I think maybe I don't know what "non-synthetic" members are...)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

By "non-synthetic" I guess I just meant the implicit default constructors.

Updated the documentation to talk more about inheritance.


const String debugAssert = '';

String globalVaraibleFromDebugLib = '';
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: 'Variable'

/// inside asserts.
///
/// The annotation can also be applied to [InterfaceElement]s (classes, mixins
/// and extensions) or libraries, in which case all non-synthetic elements
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: extensions are not InterfaceElements.


// Subclasses may "inherit" default constructors from the superclass. Since
// constructors can't be invoked by the class members (unlike methods that
// can have "bad annotations"). Defer to the first non-synthetic default
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure where this sentence starts/stops. Should it be, "), defer to ..."?

assert(!constructorElement.isFactory);
assert(constructorElement.isDefaultConstructor);

// Subclasses may "inherit" default constructors from the superclass. Since
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this is correct, even with the quotes. Classes in no way inherit constructors.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If a class doesn't define any constructors then you get class.new(). for free which invokes super() (I think that's the case?) is what I meant. Updated the comment to be more specific.


// Subclasses may "inherit" default constructors from the superclass. Since
// constructors can't be invoked by the class members (unlike methods that
// can have "bad annotations"). Defer to the first non-synthetic default
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, why the first non-synthetic (which I think means "non-default"? Take this example:

class A {
  @debugAssert
  A();
}
class B extends A {
  B();
}
class C extends B {}
void main() {
  C();
}

It seems that the call to C() should be reported even though the first non-default super-constructor called is B() which has no annotation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this case will be caught by the code in visitConstructorDeclaration, since B.new is not debug-only but A.new is, and B.new invokes A.new implicitly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

regarding first non-synthetic, I would like to make this annotation more forgiving, so in this case

class A { 
  @debugAssert
  A();
}
class B1 { 
}
class B2 extends A { 
  B2(); // illegal, implicitly invokes A.new and doesn't have @debugAssert
}

you don't have to add a default constructor to B1 to add the annotation to it.

// constructor in the class hierarchy.
final ConstructorElement? superConstructor = constructorElement.enclosingElement.thisType
.superclass?.element.constructors
.firstWhereOrNull((ConstructorElement constructor) => constructor.isDefaultConstructor);
Copy link
Contributor

Choose a reason for hiding this comment

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

Might be better performance to just ask if the unnamed constructor is default, instead of walking all of the named ones.

https://pub.dev/documentation/analyzer/latest/dart_element_element/InterfaceElement/unnamedConstructor.html

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm a bit scared of the TODO(scheglov) Deprecate and remove it. comment on this property, it sounds like unnamedConstructor will be removed soon?

// (see the non-static ExecutableElement case), but a default synthetic
// constructor should defer to its superclass since it's not explictly
// defined.
case ConstructorElement(isSynthetic: true):
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure why the synthetic nature makes a difference. Shouldn't this code be reported?

class C {
  @debugAssert
  C();
}

class D extends C {
  D(); // Implicitly calls C().
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See #130101 (comment)

Also that code will be reported I think? Neither C.new nor D.new is synthetic here?

@LongCatIsLooong
Copy link
Contributor Author

@srawlins thanks for the review! Ideally I would like to also check if a piece of code is reachable when a const variable, or a set of const variables are set to certain values, is there an easy way to do so? E.g.,

if (_kReleaseMode && ....) {
  // debug only code
}

@flutter-dashboard
Copy link

This pull request executed golden file tests, but it has not been updated in a while (20+ days). Test results from Gold expire after as many days, so this pull request will need to be updated with a fresh commit in order to get results from Gold.

For more guidance, visit Writing a golden file test for package:flutter.

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

@christopherfujino
Copy link
Contributor

Hey @LongCatIsLooong, I'm excited about this change, and would like to add my own custom rules for the tool. Are you still actively working on this?

auto-submit bot pushed a commit that referenced this pull request Dec 11, 2023
Extacted from #130101, dropped the `@_debugAssert` stuff from that PR so it's easier to review.
@github-actions github-actions bot removed a: animation Animation APIs f: material design flutter/packages/flutter/material repository. f: scrolling Viewports, list views, slivers, etc. f: cupertino flutter/packages/flutter/cupertino repository labels Feb 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a: text input Entering text in a text field or keyboard related problems c: contributor-productivity Team-specific productivity, code health, technical debt. framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Create linter that flags Double.clamp usage

5 participants