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

FED-1885: Prop requiredness codemod #290

Merged
merged 91 commits into from
Aug 10, 2024

Conversation

greglittlefield-wf
Copy link
Contributor

@greglittlefield-wf greglittlefield-wf commented Jul 31, 2024

Motivation

over_react consumers migrating to null safety will need to update each prop to be either required or optional.

Determining the correct requiredness for each prop is tedious to do manually, so we want to help automate this process, similar to how Dart's null safety migrator tool helps determine correct nullability for other code.

Changes

  • Add null_safety_required_props executable with the following commands (more info below):
    • collect - Collects requiredness data for all OverReact props based on usages in the specified packages and all their transitive dependencies.
    • codemod - Adds null safety migrator hints to OverReact props using prop requiredness data from 'collect' command.
  • Add tests
    • End-to-end tests for codemod command using collect command data: required_props_collect_and_codemod_test.dart
    • Test for data collection in various scenarios collect command: required_props_collect_test.dart
    • Unit tests for new argument removal utilities
  • Misc changes
    • dfdeacf Fix required hint util checking comments for subsequent nodes
    • Split out getAllPropsClassesOrMixins method from getAllProps
    • Set up json_serializable codegen, with optimized *.sg.dart build.yaml configuration and CI check for outdated generated files

collect command

High-level overview diagram

required-props-collect-command

Help output, containing instructions
Collects requiredness data for all OverReact props based on usages in the specified packages and all their transitive dependencies.

Usage: null_safety_required_props collect [<options>] <package_spec> [<package_spec>...]
-h, --help                         Print this usage information.
    --raw-data-output-directory    An optional directory to output raw usage data file to.
-o, --output=<path>                The file to write aggregated results to.
                                   (defaults to "prop_requiredness.json")
    --verbose                      Enable verbose output.

Run "null_safety_required_props help" to see global options.

Supported package spec formats:
- Hosted pub package with optional version (uses latest if omitted):
    - `[email protected]:over_react`
    - `[email protected]:over_react#5.2.0`
- Git URL with optional revision:
    - `[email protected]:Workiva/over_react.git`
    - `https://github.com/Workiva/over_react.git`
    - `[email protected]:Workiva/over_react.git#5.2.0`
- Local file path:
    - `/path/to/over_react`
    - `file:///path/to/over_react`

Instructions
============

1. First, identify the least-common consumer(s) of OverReact components exposed by your package.

   (If all your package's components are private, you can skip the rest of this step,
   step and just use your package).

   For example, say we're dealing with package A, which is directly consumed by
   packages B, E, and F, and so on:

       A---B---C---D
       |\     /
       | E----
       \
        F---G---H

   The least-common consumers would be C (covers both B and E) and F, so we'd run:

      null_safety_required_props collect pub@…:C pub@…:F

   Note: if F were to re-export members of A, which could potentially get used
   in G, we'd do G instead of F.

      null_safety_required_props collect pub@…:C pub@…:G

   Alternatively, we could just run on D and H from the start, but if those
   packages include more transitive dependencies, then the analysis step of the
   collection process will take a bit longer.

2. If step 1 yielded more than one package, make sure all of them can resolve to
   the latest version of your package.

   If they can't, then data may be missing for recently-added props, or could be
   incorrect if props in your package were moved to different files.

   If you're not sure, try either:
   - Cloning those packages, ensuring they resolve to the latest locally,
     and providing them as local path package specs.
   - Running the command and verifying the package versions in the command output
     line up.

3. Run the 'null_safety_required_props collect' command with the packages from step 1, using
   one of the package specifier formats listed above.

4. Use the `codemod` command within the package you want to update
   (see that command's --help for instructions):

      cd my_package
      null_safety_required_props codemod --help

codemod command

Help output, containing instructions
Adds null safety migrator hints to OverReact props using prop requiredness data from 'collect' command.

Usage: null_safety_required_props codemod [<options>]
-h, --help                               Print this usage information.
    --prop-requiredness-data             The file containing prop requiredness data, collected via the 'over_react_codemod:collect' command.
                                         (defaults to "prop_requiredness.json")
    --[no-]trust-required-annotations    Whether to migrate @requiredProp and `@nullableRequiredProp` props to late required, regardless of usage data.
                                         Note that @requiredProp has no effect on function components, so these annotations may be incorrect.
                                         (defaults to on)
    --private-requiredness-threshold     The minimum rate (0.0-1.0) a private prop must be set to be considered required.
                                         (defaults to "0.95")
    --private-max-allowed-skip-rate      The maximum allowed rate (0.0-1.0) of dynamic usages of private mixins, for which data collection was skipped.
                                         If above this, all props in a mixin will be made optional (with a TODO comment).
                                         (defaults to "0.2")
    --public-requiredness-threshold      The minimum rate (0.0-1.0) a public prop must be set to be considered required.
                                         (defaults to "1")
    --public-max-allowed-skip-rate       The maximum allowed rate (0.0-1.0) of dynamic usages of public mixins, for which data collection was skipped.
                                         If above this, all props in a mixin will be made optional (with a TODO comment).
                                         (defaults to "0.05")

Codemod options
-v, --verbose                            Outputs all logging to stdout/stderr.
    --yes-to-all                         Forces all patches accepted without prompting the user. Useful for scripts.
    --fail-on-changes                    Returns a non-zero exit code if there are changes to be made. Will not make any changes (i.e. this is a dry-run).
    --stderr-assume-tty                  Forces ansi color highlighting of stderr. Useful for debugging.

Run "null_safety_required_props help" to see global options.

Instructions
============

1. First, run the 'collect' command to collect data on usages of props declared
   in your package (see that command's --help for instructions).

    null_safety_required_props collect --help

2. Run this command within the package you want to update:

    null_safety_required_props codemod

3. Inspect the TODO comments left over from the codemod. If you want to adjust
   any thresholds or re-collect data, discard changes before re-running the codemod.

4. Commit the changes made by the codemod.

5. Proceed with using the Dart null safety migrator tool to migrate your code.

6. Review TODO comments, adjusting requiredness if desired. You can use a
   find-replace with the following regex to remove them:

       ^ *// TODO\(orcm.required_props\):.+(?:\n *//  .+)*

Release Notes

Review

See CONTRIBUTING.md for more details on review types (+1 / QA +1 / +10) and code review process.

Please review:

QA Instructions

To run the executable locally, either:

  • Use dpx to run from this branch:
    dpx github:Workiva/over_react_codemod#ref=prop-requiredness-codemod-with-collection:null_safety_required_props
  • Clone locally, and within over_react_codemod run dart pub global --source path .. Then, you can run null_safety_required_props or dart pub global run over_react_codemod:null_safety_required_props

QA steps:

  • CI passes
  • Verify help output for each subcommand is well-formatted
  • Verify all supported package spec formats (from help comment) work for collect command
  • Run collect and codemod commands for a package that has at least some public components and verify output looks good

Merge Checklist

While we perform many automated checks before auto-merging, some manual checks are needed:

  • A Frontend Frameworks Design member has reviewed these changes
  • There are no unaddressed comments - this check can be automated if reviewers use the "Request Changes" feature
  • For release PRs - Version metadata in Rosie comment is correct

@greglittlefield-wf greglittlefield-wf marked this pull request as ready for review August 5, 2024 19:35
@greglittlefield-wf greglittlefield-wf force-pushed the prop-requiredness-codemod-with-collection branch from 6182a4a to 407ea41 Compare August 6, 2024 17:03
@greglittlefield-wf
Copy link
Contributor Author

@aaronlademann-wf @sydneyjodon-wk Alright, all the items that came up during our meeting yesterday should be addressed now

Copy link
Contributor

@sydneyjodon-wk sydneyjodon-wk left a comment

Choose a reason for hiding this comment

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

QA +1 I tested it out and it worked great!! This is awesome!

Comment on lines +61 to +75
For example, say we're dealing with package A, which is directly consumed by
packages B, E, and F, and so on:

${r'A---B---C---D'}
${r'|\ /'}
${r'| E----'}
${r'\'}
${r' F---G---H'}

The least-common consumers would be C (covers both B and E) and F, so we'd run:

$invocationPrefix pub@…:C pub@…:F

Note: if F were to re-export members of A, which could potentially get used
in G, we'd do G instead of F.
Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry I still don't get this. I understand the last Alternatively,... would just be doing wdesk or something, but I don't really understand what on the graph makes it okay to do C and F

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry about that; I was trying to capture that there may be branches of downstream consumers that aren't related. For instance, in this example, there's no package that transitively consumes both C and F.

Was that the part you weren't following, or maybe something else? Happy to hop on a call and talk through it too

And I'm totally down to adjust this example however needed to make things clearer

Copy link
Contributor

@sydneyjodon-wk sydneyjodon-wk Aug 8, 2024

Choose a reason for hiding this comment

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

Yeah that makes sense, I guess I just don't get why it can be F and C and not always D and H

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh it can be D and H, the idea is just that F and C is preferred (mainly because we get to skip analyzing code for D, H, G, and any other transitive dependencies not pulled in by F or 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.

...And also increases the risk of mismatched dependencies talked about in the next step:

  1. If step 1 yielded more than one package, make sure all of them can resolve to
    the latest version of your package.

If they can't, then data may be missing for recently-added props, or could be
incorrect if props in your package were moved to different files.

Copy link
Contributor

Choose a reason for hiding this comment

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

oh wait I get it - I was misunderstanding the transitive dependency thing and that D and H aren't directly consuming A - cool cool that makes sense! Thank you!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sweet, np! If you have any suggestions on how to make the wording or diagram more clear, I'm all ears!

Copy link
Contributor

@aaronlademann-wf aaronlademann-wf left a comment

Choose a reason for hiding this comment

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

+1 #nothinbutnits

This is impressive stuff @greglittlefield-wf!!!

When we don't have use for this specific codemod anymore - we should make sure to keep some of the code you wrote here because I could see a lot of it being reusable for other mods in the future!

final commentContents =
"$_todoWithPrefix: No data for prop; either it's never set,"
" all places it was set were on dynamic usages,"
" or requiredness data was collected on a version before this prop was added.";
Copy link
Contributor

Choose a reason for hiding this comment

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

Wow - talk about being thorough...

He is very thorough

Comment on lines +180 to +190
extension on Token {
/// The offset of the next token or comment
/// (since comments can occur before the next token)
/// following this token, or null if nothing follows it.
int? get nextTokenOrCommentOffset {
final next = this.next;
if (next == null) return null;
final nextTokenOrComment = next.precedingComments ?? next;
return nextTokenOrComment.offset;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like we have this - or something like this - in like 5 or 6 codemods lol

final isSamePackage = usage.usagePackage == mixin.mixinPackage;

final categorizedStats = usageStatsByMixinId.putIfAbsent(
mixin.mixinId, CategorizedPropsMixinUsageStats.new);
Copy link
Contributor

Choose a reason for hiding this comment

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

TIL you can use new for a class constructor tearoff!??!?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup, as of language version 2.15!! Soo nice

),
);

logger.finer('Aggregating final results...');
Copy link
Contributor

Choose a reason for hiding this comment

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

drumroll


logProgress();

//logger.finest('Processing ${unitResult.uri}');
Copy link
Contributor

Choose a reason for hiding this comment

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

#nit

Comment on lines +97 to +99
.where((element) => element.isGetter)
// Note that this is public relative to the library, not necessarily the package.
.where((element) => element.isPublic)
Copy link
Contributor

Choose a reason for hiding this comment

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

#supernit

Suggested change
.where((element) => element.isGetter)
// Note that this is public relative to the library, not necessarily the package.
.where((element) => element.isPublic)
.where((element) => element.isGetter && element.isPublic)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah a lot of times I like splitting these out separately for organizational purposes

final assignedPropNames = assignedProps.map((p) => p.name.name).toSet();
final unaccountedForPropNames = {...assignedPropNames};

// TODO maybe store mixin metadata separately?
Copy link
Contributor

Choose a reason for hiding this comment

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

#nit

@@ -8,30 +8,35 @@ description: >


environment:
sdk: '>=2.12.0 <3.0.0'
sdk: '>=2.15.0 <3.0.0'
Copy link
Contributor

Choose a reason for hiding this comment

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

Just curious, why not 2.19.6?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah in this case I probably could have raised it higher, but didn't wanna mess with any potential breakages caused by newer language versions (particularly 2.18).

If I did raise it, I'd probably 2.19.0 to not unnecessarily restrict it, unless the package really needed the patches

@greglittlefield-wf
Copy link
Contributor Author

@Workiva/release-management-p

Copy link

@rmconsole-wf rmconsole-wf left a comment

Choose a reason for hiding this comment

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

+1 from RM

@rmconsole2-wf rmconsole2-wf merged commit d76eaa2 into master Aug 10, 2024
6 checks passed
@rmconsole2-wf rmconsole2-wf deleted the prop-requiredness-codemod-with-collection branch August 10, 2024 00:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants