├── .github
└── workflows
│ └── dart.yml
├── .gitignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── analysis_options.yaml
├── example
├── annotation_surveyor.dart
├── api_surveyor.dart
├── async_surveyor.dart
├── core_lib_use_surveyor.dart
├── deprecated_api_surveyor.dart
├── doc_surveyor
│ ├── analysis_options.yaml
│ ├── lib
│ │ ├── main.dart
│ │ └── src
│ │ │ └── doc_surveyor.dart
│ └── pubspec.yaml
├── error_surveyor.dart
├── lint_surveyor.dart
├── options_surveyor.dart
└── widget_surveyor
│ ├── analysis_options.yaml
│ ├── lib
│ └── widget_surveyor.dart
│ ├── pubspec.yaml
│ └── test
│ ├── data
│ ├── animated_container
│ │ ├── analysis_options.yaml
│ │ ├── lib
│ │ │ └── main.dart
│ │ └── pubspec.yaml
│ ├── async
│ │ ├── lib
│ │ │ └── main.dart
│ │ └── pubspec.yaml
│ ├── basic_app
│ │ ├── analysis_options.yaml
│ │ ├── lib
│ │ │ └── main.dart
│ │ ├── pubspec.yaml
│ │ └── test
│ │ │ ├── has_error.dart
│ │ │ └── main_test.dart
│ └── route_app
│ │ ├── analysis_options.yaml
│ │ ├── lib
│ │ └── main.dart
│ │ └── pubspec.yaml
│ └── widget_surveyor_test.dart
├── lib
└── src
│ ├── analysis.dart
│ ├── common.dart
│ ├── driver.dart
│ ├── install.dart
│ └── visitors.dart
├── pubspec.yaml
├── test
└── core_lib_use_surveyor_test.dart
├── test_data
└── core_lib_use_surveyor
│ ├── lib
│ └── data.dart
│ └── pubspec.yaml
└── tool
├── de_dup.dart
├── filter_dart2.dart
├── find_authors.dart
└── spelunk.dart
/.github/workflows/dart.yml:
--------------------------------------------------------------------------------
1 | # This workflow uses actions that are not certified by GitHub.
2 | # They are provided by a third-party and are governed by
3 | # separate terms of service, privacy policy, and support
4 | # documentation.
5 |
6 | name: dart
7 |
8 | on:
9 | push:
10 | branches: [ main ]
11 | pull_request:
12 | branches: [ main ]
13 |
14 | jobs:
15 | build:
16 | runs-on: ubuntu-latest
17 |
18 | steps:
19 | - uses: actions/checkout@v2
20 |
21 | # Note: This workflow uses the latest stable version of the Dart SDK.
22 | # You can specify other versions if desired, see documentation here:
23 | # https://github.com/dart-lang/setup-dart/blob/main/README.md
24 | # - uses: dart-lang/setup-dart@v1
25 | - uses: dart-lang/setup-dart@v1.3
26 | with:
27 | sdk: dev
28 |
29 | - name: Install package dependencies
30 | run: dart pub get
31 |
32 | - name: Install widget_surveyor example dependencies
33 | run: (cd example/widget_surveyor; dart pub get)
34 |
35 | - name: Install doc_surveyor example dependencies
36 | run: (cd example/doc_surveyor; dart pub get)
37 |
38 | - name: Verify formatting
39 | run: dart format --output=none --set-exit-if-changed .
40 |
41 | - name: Analyze project source
42 | run: dart analyze --fatal-infos
43 |
44 | - name: Run core tests
45 | run: dart test
46 |
47 | # Enable when fixed (https://github.com/pq/surveyor/issues/27)
48 | # - name: Run example tests
49 | # run: (cd example/widget_surveyor; dart test)
50 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Local package cache
2 | cache/
3 |
4 | # Files and directories created by pub
5 | .dart_tool/
6 | .packages
7 | pubspec.lock
8 |
9 | # Conventional directory for build outputs
10 | build/
11 |
12 | # Directory created by dartdoc
13 | doc/api/
14 |
15 | # Cache directories
16 | /third_party/cache
17 | /third_party/index.json
18 |
19 | # IDEA
20 |
21 | /surveyor.iml
22 | /.idea
23 |
24 | # macOS
25 | .DS_Store
26 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 1.0.0-dev.3.0
2 |
3 | * Upgrade to cli_util ^0.4.0
4 |
5 | # 1.0.0-dev.2.0
6 |
7 | * Migrate to analyzer `5.4.0` APIs.
8 |
9 | # 1.0.0-dev.1.0
10 |
11 | * Initial dev release.
12 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to Contribute
2 |
3 | We'd love to accept your patches and contributions to this project. There are
4 | just a few small guidelines you need to follow.
5 |
6 | ## Contributor License Agreement
7 |
8 | Contributions to this project must be accompanied by a Contributor License
9 | Agreement. You (or your employer) retain the copyright to your contribution;
10 | this simply gives us permission to use and redistribute your contributions as
11 | part of the project. Head over to to see
12 | your current agreements on file or to sign a new one.
13 |
14 | You generally only need to submit a CLA once, so if you've already submitted one
15 | (even if it was for a different project), you probably don't need to do it
16 | again.
17 |
18 | ## Code reviews
19 |
20 | All submissions, including submissions by project members, require review. We
21 | use GitHub pull requests for this purpose. Consult
22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
23 | information on using pull requests.
24 |
25 | ## Community Guidelines
26 |
27 | This project follows
28 | [Google's Open Source Community Guidelines](https://opensource.google.com/conduct/).
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 📐 surveyor
2 | Tools for surveying Dart packages.
3 |
4 | [](https://github.com/pq/surveyor/actions)
5 | [](https://pub.dartlang.org/packages/surveyor)
6 |
7 |
8 | **Disclaimer:** This is not an officially supported Google product.
9 |
10 | ## Installing
11 |
12 | These tools are best run from source. To get the sources, clone the `surveyor` repo like this:
13 |
14 | $ git clone https://github.com/pq/surveyor.git
15 |
16 | From there you can run the `examples`.
17 |
18 | ## Examples
19 |
20 | ### Surveying API Use
21 |
22 | dart run example/api_surveyor.dart
23 |
24 | will analyze projects at the given path and identify uses of a few specific APIs.
25 |
26 | ### Surveying `async` Identifier Use
27 |
28 | dart run example/async_surveyor.dart
29 |
30 | will analyze projects at the given path and identify places where `"async"` is used as a simple identifer. These places would produce errors if `async` become a reserved keyword.
31 |
32 | Note that this generates a lot of output. To make sure none of it is lost, consider redirecting to a file. For example:
33 |
34 | dart run example/async_surveyor.dart 2>&1 | tee survey_out.txt
35 |
36 | ### Surveying Errors
37 |
38 | dart run example/error_surveyor.dart
39 |
40 | will analyze projects at the given path, filtering for errors.
41 |
42 | ### Surveying Lint Rule Violations
43 |
44 | dart run example/lint_surveyor.dart
45 |
46 | will analyze projects at the given path and identify violations of lint rules (custom rules or ones defined by `package:linter`).
47 |
48 | ### Surveying API Doc Scoring
49 |
50 | dart run example/doc_surveyor/lib/main.dart
51 |
52 | will analyze the project at the given path flagging public members that are missing API docs.
53 |
54 | A sample run produces output like this:
55 |
56 | ```
57 | 122 public members
58 | Members without docs:
59 | Void • /packages/provider/lib/src/proxy_provider.dart • 107:1
60 | NumericProxyProvider • /packages/provider/lib/src/proxy_provider.dart • 177:1
61 | Score: 0.98
62 | ```
63 |
64 | ### Surveying Widget Use
65 |
66 | dart run example/widget_surveyor/lib/widget_surveyor.dart
67 |
68 | will analyze the project at the given path and present a list of found `Widget` child-parent 2-Grams.
69 |
70 | A sample run produces a csv file with contents like this:
71 |
72 | ```
73 | AppBar -> Text, 1
74 | Center -> Column, 1
75 | Column -> Text, 3
76 | FloatingActionButton -> Icon, 1
77 | MaterialApp -> MyHomePage, 1
78 | Scaffold -> AppBar, 1
79 | Scaffold -> Center, 1
80 | Scaffold -> FloatingActionButton, 1
81 | null -> MaterialApp, 1
82 | null -> MyApp, 1
83 | null -> Scaffold, 1
84 | ```
85 |
86 | (Note that by default package dependencies will only be installed if a `.packages` file is absent from the project under analysis. If you want to make sure package dependencies are (re)installed, run with the `--force-install` option.)
87 |
88 | ## Related Work
89 |
90 | See also [`package:pub_crawl`][pub_crawl], which can be used to fetch package sources for analysis from pub.
91 |
92 | ## Features and bugs
93 |
94 | This is very much a work in progress. Please file feature requests, bugs and any feedback in the [issue tracker][tracker].
95 |
96 | Thanks!
97 |
98 | [tracker]: https://github.com/pq/surveyor/issues
99 | [pub_crawl]: https://github.com/pq/pub_crawl
100 |
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | include: package:lints/recommended.yaml
2 |
3 | analyzer:
4 | exclude:
5 | - test/data/**
6 |
7 | linter:
8 | rules:
9 | - annotate_overrides
10 | - avoid_bool_literals_in_conditional_expressions
11 | - avoid_function_literals_in_foreach_calls
12 | - avoid_null_checks_in_equality_operators
13 | # - avoid_print # ensure all calls are rationalized (and silence-able)
14 | - avoid_private_typedef_functions
15 | - avoid_redundant_argument_values
16 | - await_only_futures
17 | - camel_case_types
18 | - comment_references
19 | - depend_on_referenced_packages
20 | - directives_ordering
21 | - empty_statements
22 | - hash_and_equals
23 | - implementation_imports
24 | - non_constant_identifier_names
25 | - omit_local_variable_types
26 | - prefer_adjacent_string_concatenation
27 | - prefer_collection_literals
28 | - prefer_conditional_assignment
29 | - prefer_expression_function_bodies
30 | - prefer_final_fields
31 | - prefer_for_elements_to_map_fromIterable
32 | - prefer_function_declarations_over_variables
33 | - prefer_generic_function_type_aliases
34 | - prefer_if_null_operators
35 | - prefer_initializing_formals
36 | - prefer_inlined_adds
37 | - prefer_interpolation_to_compose_strings
38 | - prefer_mixin
39 | - prefer_null_aware_operators
40 | - prefer_relative_imports
41 | - prefer_single_quotes
42 | - prefer_spread_collections
43 | - prefer_void_to_null
44 | - provide_deprecation_message
45 | - test_types_in_equals
46 | - throw_in_finally
47 | - unawaited_futures
48 | - unnecessary_brace_in_string_interps
49 | - unnecessary_final
50 | - unnecessary_getters_setters
51 | - unnecessary_lambdas
52 | - unnecessary_overrides
53 | - unnecessary_parenthesis
54 | - unnecessary_statements
55 | - unnecessary_this
56 | - use_full_hex_values_for_flutter_colors
57 | - use_function_type_syntax_for_parameters
58 | - use_string_buffers
59 | - void_checks
60 |
--------------------------------------------------------------------------------
/example/annotation_surveyor.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import 'dart:io';
16 |
17 | import 'package:analyzer/dart/ast/ast.dart';
18 | import 'package:analyzer/dart/ast/visitor.dart';
19 | import 'package:analyzer/file_system/file_system.dart' hide File;
20 | import 'package:analyzer/source/line_info.dart';
21 | import 'package:path/path.dart' as path;
22 | import 'package:surveyor/src/driver.dart';
23 | import 'package:surveyor/src/visitors.dart';
24 |
25 | /// Looks for a few specific API uses.
26 | ///
27 | /// Run like so:
28 | ///
29 | /// dart run example/annotation_surveyor.dart
30 | void main(List args) async {
31 | if (args.length == 1) {
32 | var dir = args[0];
33 | if (!File('$dir/pubspec.yaml').existsSync()) {
34 | print("Recursing into '$dir'...");
35 | args = Directory(dir).listSync().map((f) => f.path).toList()..sort();
36 | dirCount = args.length;
37 | print('(Found $dirCount subdirectories.)');
38 | }
39 | }
40 |
41 | if (_debugLimit != 0) {
42 | print('Limiting analysis to $_debugLimit packages.');
43 | }
44 |
45 | var collector = AnnotationUseCollector();
46 |
47 | var stopwatch = Stopwatch()..start();
48 |
49 | var driver = Driver.forArgs(args);
50 | driver.forceSkipInstall = true;
51 | driver.showErrors = false;
52 | driver.resolveUnits = true;
53 | driver.visitor = collector;
54 |
55 | await driver.analyze();
56 |
57 | stopwatch.stop();
58 |
59 | var duration = Duration(milliseconds: stopwatch.elapsedMilliseconds);
60 | var aliasCount = collector.aliasCount;
61 | var typeCount = collector.genericFunctionType.alias;
62 | var parameterCount = collector.parameterCount;
63 | var parameterWithMetadataCount = collector.parameterWithMetadataCount;
64 | var percent = collector.parameterPercents;
65 |
66 | print('(Elapsed time: $duration)');
67 | print('');
68 | print('$percent of parameters in function type aliases have annotations');
69 | print('');
70 | print('Found $aliasCount function type aliases');
71 | print('Found $typeCount generic function types not in aliases');
72 | print('Found $parameterCount parameters in those aliases and function types');
73 | print('Found $parameterWithMetadataCount parameters with annotations');
74 | print('');
75 | print('Notes:');
76 | print('- Numbers are for all function types, and are followed by a');
77 | print(' breakdown with the numbers for old-style function type aliases');
78 | print(' first, followed by the numbers for generic function type aliases,');
79 | print(' followed by the numbers for generic function types outside of');
80 | print(' aliases (when appropriate).');
81 | }
82 |
83 | int dirCount = 0;
84 |
85 | /// If non-zero, stops once limit is reached (for debugging).
86 | int _debugLimit = 0; //500;
87 |
88 | class AnnotationUseCollector extends RecursiveAstVisitor
89 | implements PreAnalysisCallback, PostAnalysisCallback, AstContext {
90 | int count = 0;
91 | String? filePath;
92 | LineInfo? lineInfo;
93 | Folder? currentFolder;
94 |
95 | Counts functionTypeAlias = Counts();
96 | Counts genericTypeAlias = Counts();
97 | Counts genericFunctionType = Counts();
98 |
99 | AnnotationUseCollector();
100 |
101 | String get aliasCount {
102 | var function = functionTypeAlias.alias;
103 | var generic = genericTypeAlias.alias;
104 | return '${function + generic} ($function, $generic)';
105 | }
106 |
107 | String get parameterCount {
108 | var function = functionTypeAlias.parameter;
109 | var generic = genericTypeAlias.parameter;
110 | var type = genericFunctionType.parameter;
111 | return '${function + generic + type} ($function, $generic, $type)';
112 | }
113 |
114 | String get parameterPercents {
115 | String percent(int numerator, int denominator) {
116 | if (denominator == 0) {
117 | return '0.00';
118 | }
119 | var percent = numerator / denominator;
120 | return ((percent * 10000).truncate() / 100).toStringAsFixed(2);
121 | }
122 |
123 | var functionNumerator = functionTypeAlias.parameterWithMetadata;
124 | var functionDenominator = functionTypeAlias.parameter;
125 | var functionPercent = percent(functionNumerator, functionDenominator);
126 |
127 | var genericNumerator = genericTypeAlias.parameterWithMetadata;
128 | var genericDenominator = genericTypeAlias.parameter;
129 | var genericPercent = percent(genericNumerator, genericDenominator);
130 |
131 | var typeNumerator = genericFunctionType.parameterWithMetadata;
132 | var typeDenominator = genericFunctionType.parameter;
133 | var typePercent = percent(typeNumerator, typeDenominator);
134 |
135 | var totalNumerator = functionNumerator + typeNumerator + genericNumerator;
136 | var totalDenominator =
137 | functionDenominator + typeDenominator + genericDenominator;
138 | var totalPercent = percent(totalNumerator, totalDenominator);
139 |
140 | return '$totalPercent% ($functionPercent%, $genericPercent%, $typePercent%)';
141 | }
142 |
143 | String get parameterWithMetadataCount {
144 | var function = functionTypeAlias.parameterWithMetadata;
145 | var generic = genericTypeAlias.parameterWithMetadata;
146 | var type = genericFunctionType.parameterWithMetadata;
147 | return '${function + generic + type} ($function, $generic, $type)';
148 | }
149 |
150 | @override
151 | void postAnalysis(SurveyorContext context, DriverCommands cmd) {
152 | var debugLimit = _debugLimit;
153 | cmd.continueAnalyzing = debugLimit == 0 || count < debugLimit;
154 | // Reporting done in visitSimpleIdentifier.
155 | }
156 |
157 | @override
158 | void preAnalysis(SurveyorContext context,
159 | {bool? subDir, DriverCommands? commandCallback}) {
160 | if (subDir ?? false) {
161 | ++dirCount;
162 | }
163 | var contextRoot = context.analysisContext.contextRoot;
164 | currentFolder = contextRoot.root;
165 | var dirName = path.basename(contextRoot.root.path);
166 |
167 | print("Analyzing '$dirName' • [${++count}/$dirCount]...");
168 | }
169 |
170 | @override
171 | void setFilePath(String filePath) {
172 | this.filePath = filePath;
173 | }
174 |
175 | @override
176 | void setLineInfo(LineInfo lineInfo) {
177 | this.lineInfo = lineInfo;
178 | }
179 |
180 | @override
181 | void visitFunctionTypeAlias(FunctionTypeAlias node) {
182 | functionTypeAlias.countParameters(node.parameters.parameters);
183 | return super.visitFunctionTypeAlias(node);
184 | }
185 |
186 | @override
187 | void visitGenericFunctionType(GenericFunctionType node) {
188 | if (node.parent is! GenericTypeAlias) {
189 | genericFunctionType.countParameters(node.parameters.parameters);
190 | }
191 | super.visitGenericFunctionType(node);
192 | }
193 |
194 | @override
195 | void visitGenericTypeAlias(GenericTypeAlias node) {
196 | var parameters = node.functionType?.parameters.parameters;
197 | if (parameters != null) {
198 | genericTypeAlias.countParameters(parameters);
199 | }
200 | return super.visitGenericTypeAlias(node);
201 | }
202 | }
203 |
204 | class Counts {
205 | /// The number of type aliases that were visited.
206 | int alias = 0;
207 |
208 | /// The number of parameters in type aliases that were visited.
209 | int parameter = 0;
210 |
211 | /// The number of parameters in type aliases that were visited that had
212 | /// annotations associated with them.
213 | int parameterWithMetadata = 0;
214 |
215 | Counts();
216 |
217 | void countParameters(List parameters) {
218 | alias++;
219 | parameter += parameters.length;
220 | for (var parameter in parameters) {
221 | if (parameter.metadata.isNotEmpty) {
222 | parameterWithMetadata++;
223 | }
224 | }
225 | }
226 | }
227 |
--------------------------------------------------------------------------------
/example/api_surveyor.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import 'dart:io';
16 |
17 | import 'package:analyzer/dart/ast/ast.dart';
18 | import 'package:analyzer/dart/ast/visitor.dart';
19 | import 'package:analyzer/file_system/file_system.dart' hide File;
20 | import 'package:analyzer/source/line_info.dart';
21 | import 'package:path/path.dart' as path;
22 | import 'package:surveyor/src/driver.dart';
23 | import 'package:surveyor/src/visitors.dart';
24 |
25 | /// Looks for a few specific API uses.
26 | ///
27 | /// Run like so:
28 | ///
29 | /// dart run example/api_surveyor.dart
30 | void main(List args) async {
31 | if (args.length == 1) {
32 | var dir = args[0];
33 | if (!File('$dir/pubspec.yaml').existsSync()) {
34 | print("Recursing into '$dir'...");
35 | args = Directory(dir).listSync().map((f) => f.path).toList()..sort();
36 | dirCount = args.length;
37 | print('(Found $dirCount subdirectories.)');
38 | }
39 | }
40 |
41 | if (_debugLimit != 0) {
42 | print('Limiting analysis to $_debugLimit packages.');
43 | }
44 |
45 | var driver = Driver.forArgs(args);
46 | driver.forceSkipInstall = true;
47 | driver.showErrors = false;
48 | driver.resolveUnits = true;
49 | driver.visitor = ApiUseCollector();
50 |
51 | await driver.analyze(displayTiming: true);
52 | }
53 |
54 | int dirCount = 0;
55 |
56 | /// If non-zero, stops once limit is reached (for debugging).
57 | int _debugLimit = 0; //500;
58 |
59 | class ApiUseCollector extends RecursiveAstVisitor
60 | implements PreAnalysisCallback, PostAnalysisCallback, AstContext {
61 | int count = 0;
62 | int contexts = 0;
63 | String? filePath;
64 | Folder? currentFolder;
65 | LineInfo? lineInfo;
66 |
67 | List reports = [];
68 |
69 | ApiUseCollector();
70 |
71 | @override
72 | void postAnalysis(SurveyorContext context, DriverCommands cmd) {
73 | var debugLimit = _debugLimit;
74 | cmd.continueAnalyzing = debugLimit == 0 || count < debugLimit;
75 | // Reporting done in visitSimpleIdentifier.
76 | }
77 |
78 | @override
79 | void preAnalysis(SurveyorContext context,
80 | {bool? subDir, DriverCommands? commandCallback}) {
81 | if (subDir ?? false) {
82 | ++dirCount;
83 | }
84 | var contextRoot = context.analysisContext.contextRoot;
85 | currentFolder = contextRoot.root;
86 | var dirName = path.basename(contextRoot.root.path);
87 |
88 | print("Analyzing '$dirName' • [${++count}/$dirCount]...");
89 | }
90 |
91 | @override
92 | void setFilePath(String filePath) {
93 | this.filePath = filePath;
94 | }
95 |
96 | @override
97 | void setLineInfo(LineInfo lineInfo) {
98 | this.lineInfo = lineInfo;
99 | }
100 |
101 | @override
102 | void visitMethodInvocation(MethodInvocation node) {
103 | var lineInfo = this.lineInfo;
104 | if (lineInfo == null) return;
105 |
106 | CharacterLocation? location;
107 | var name = node.methodName.name;
108 | if (name == 'transform' || name == 'pipe') {
109 | var type = node.realTarget?.staticType?.element?.name;
110 | if (type == 'Stream') {
111 | location = lineInfo.getLocation(node.offset);
112 | }
113 | } else if (name == 'close') {
114 | var type = node.realTarget?.staticType?.element?.name;
115 | if (type == 'HttpClientRequest') {
116 | location = lineInfo.getLocation(node.offset);
117 | }
118 | }
119 |
120 | if (location != null) {
121 | print(
122 | '${node.staticType?.element?.name}.$name: $filePath:${location.lineNumber}:${location.columnNumber}');
123 | }
124 |
125 | super.visitMethodInvocation(node);
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/example/async_surveyor.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import 'dart:io';
16 |
17 | import 'package:analyzer/dart/ast/ast.dart';
18 | import 'package:analyzer/dart/ast/visitor.dart';
19 | import 'package:analyzer/file_system/file_system.dart' hide File;
20 | import 'package:analyzer/source/line_info.dart';
21 | import 'package:path/path.dart' as path;
22 | import 'package:surveyor/src/driver.dart';
23 | import 'package:surveyor/src/visitors.dart';
24 |
25 | /// Looks for instances where "async" is used as an identifier
26 | /// and would break were it made a keyword.
27 | ///
28 | /// Run like so:
29 | ///
30 | /// dart run example/async_surveyor.dart
31 | void main(List args) async {
32 | if (args.length == 1) {
33 | var dir = args[0];
34 | if (!File('$dir/pubspec.yaml').existsSync()) {
35 | print("Recursing into '$dir'...");
36 | args = Directory(dir).listSync().map((f) => f.path).toList()..sort();
37 | dirCount = args.length;
38 | print('(Found $dirCount subdirectories.)');
39 | }
40 | }
41 |
42 | if (_debugLimit != 0) {
43 | print('Limiting analysis to $_debugLimit packages.');
44 | }
45 |
46 | var driver = Driver.forArgs(args);
47 | driver.forceSkipInstall = true;
48 | driver.showErrors = false;
49 | driver.resolveUnits = false;
50 | driver.visitor = AsyncCollector();
51 |
52 | await driver.analyze();
53 | }
54 |
55 | int dirCount = 0;
56 |
57 | /// If non-zero, stops once limit is reached (for debugging).
58 | int _debugLimit = 0; //500;
59 |
60 | class AsyncCollector extends RecursiveAstVisitor
61 | implements
62 | PostVisitCallback,
63 | PreAnalysisCallback,
64 | PostAnalysisCallback,
65 | AstContext {
66 | int count = 0;
67 | int contexts = 0;
68 | String? filePath;
69 | late Folder currentFolder;
70 | LineInfo? lineInfo;
71 | Set contextRoots = {};
72 |
73 | // id: inDecl, notInDecl
74 | Map occurrences = {
75 | 'async': Occurrences(),
76 | 'await': Occurrences(),
77 | 'yield': Occurrences(),
78 | };
79 |
80 | List reports = [];
81 |
82 | AsyncCollector();
83 |
84 | @override
85 | void onVisitFinished() {
86 | print(
87 | 'Found ${reports.length} occurrences in ${contextRoots.length} packages:');
88 | reports.forEach(print);
89 |
90 | for (var o in occurrences.entries) {
91 | var data = o.value;
92 | print('${o.key}: [${data.decls} decl, ${data.notDecls} ref]');
93 | data.packages.forEach(print);
94 | }
95 | }
96 |
97 | @override
98 | void postAnalysis(SurveyorContext context, DriverCommands cmd) {
99 | var debugLimit = _debugLimit;
100 | cmd.continueAnalyzing = debugLimit == 0 || count < debugLimit;
101 | // Reporting done in visitSimpleIdentifier.
102 | }
103 |
104 | @override
105 | void preAnalysis(SurveyorContext context,
106 | {bool? subDir, DriverCommands? commandCallback}) {
107 | if (subDir ?? false) {
108 | ++dirCount;
109 | }
110 | var contextRoot = context.analysisContext.contextRoot;
111 | currentFolder = contextRoot.root;
112 | var dirName = path.basename(contextRoot.root.path);
113 |
114 | print("Analyzing '$dirName' • [${++count}/$dirCount]...");
115 | }
116 |
117 | @override
118 | void setFilePath(String filePath) {
119 | this.filePath = filePath;
120 | }
121 |
122 | @override
123 | void setLineInfo(LineInfo lineInfo) {
124 | this.lineInfo = lineInfo;
125 | }
126 |
127 | @override
128 | void visitSimpleIdentifier(SimpleIdentifier node) {
129 | var lineInfo = this.lineInfo;
130 | if (lineInfo == null) return;
131 |
132 | var id = node.name;
133 |
134 | var occurrence = occurrences[id];
135 | if (occurrence != null) {
136 | if (node.inDeclarationContext()) {
137 | occurrence.decls++;
138 | } else {
139 | occurrence.notDecls++;
140 | }
141 |
142 | // cache/flutter_util-0.0.1 => flutter_util
143 | occurrence.packages
144 | .add(currentFolder.path.split('/').last.split('-').first);
145 |
146 | var location = lineInfo.getLocation(node.offset);
147 | var report = '$filePath:${location.lineNumber}:${location.columnNumber}';
148 | reports.add(report);
149 | var declDetail = node.inDeclarationContext() ? '(decl) ' : '';
150 | print("found '$id' $declDetail• $report");
151 | contextRoots.add(currentFolder);
152 | print(occurrences);
153 | }
154 | super.visitSimpleIdentifier(node);
155 | }
156 | }
157 |
158 | class Occurrences {
159 | int decls = 0;
160 | int notDecls = 0;
161 | Set packages = {};
162 |
163 | @override
164 | String toString() => '[$decls, $notDecls] : $packages';
165 | }
166 |
--------------------------------------------------------------------------------
/example/core_lib_use_surveyor.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2022 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import 'dart:convert';
16 | import 'dart:io';
17 |
18 | import 'package:analyzer/dart/ast/ast.dart';
19 | import 'package:analyzer/dart/ast/visitor.dart';
20 | import 'package:analyzer/dart/element/element.dart';
21 | import 'package:path/path.dart' as path;
22 | import 'package:surveyor/src/driver.dart';
23 | import 'package:surveyor/src/visitors.dart';
24 |
25 | /// Collects data about core library use.
26 | ///
27 | /// Run like so:
28 | ///
29 | /// dart run example/core_lib_use_surveyor.dart
30 | void main(List args) async {
31 | if (args.length == 1) {
32 | var arg = args[0];
33 | if (arg.endsWith('.json')) {
34 | print("Parsing occurrence data in '$arg'...");
35 | var occurrences = Occurrences.fromFile(arg);
36 | displayOccurrences(occurrences);
37 | return;
38 | }
39 | }
40 |
41 | if (_debugLimit != 0) {
42 | print('Limiting analysis to $_debugLimit packages.');
43 | }
44 |
45 | await survey(args, displayTiming: true);
46 | displayOccurrences(occurrences);
47 |
48 | var file = 'example/core_lib_use_surveyor/occurrences.json';
49 | print('Writing occurrence data to $file');
50 | occurrences.toFile(file);
51 | }
52 |
53 | int dirCount = 0;
54 |
55 | var occurrences = Occurrences();
56 |
57 | /// If non-zero, stops once limit is reached (for debugging).
58 | int _debugLimit = 10; //500;
59 |
60 | void displayOccurrences(Occurrences occurrences) {
61 | print(occurrences.data);
62 | }
63 |
64 | Future survey(List args,
65 | {bool displayTiming = false}) async {
66 | if (args.length == 1) {
67 | var dir = args[0];
68 |
69 | if (!File('$dir/pubspec.yaml').existsSync()) {
70 | print("Recursing into '$dir'...");
71 | args = Directory(dir).listSync().map((f) => f.path).toList()..sort();
72 | dirCount = args.length;
73 | print('(Found $dirCount subdirectories.)');
74 | }
75 | }
76 |
77 | var driver = Driver.forArgs(args)..visitor = LibraryUseCollector();
78 | await driver.analyze(displayTiming: displayTiming);
79 |
80 | return occurrences;
81 | }
82 |
83 | class LibraryUseCollector extends RecursiveAstVisitor
84 | implements PreAnalysisCallback, PostAnalysisCallback {
85 | int count = 0;
86 | late String dirName;
87 |
88 | void add({required String? library, required String symbol}) {
89 | occurrences.add(dirName, library: library!, symbol: symbol);
90 | }
91 |
92 | @override
93 | void postAnalysis(SurveyorContext context, DriverCommands cmd) {
94 | cmd.continueAnalyzing = _debugLimit == 0 || count < _debugLimit;
95 | }
96 |
97 | @override
98 | void preAnalysis(SurveyorContext context,
99 | {bool? subDir, DriverCommands? commandCallback}) {
100 | if (subDir ?? false) {
101 | ++dirCount;
102 | }
103 | var contextRoot = context.analysisContext.contextRoot;
104 | dirName = path.basename(contextRoot.root.path);
105 |
106 | print("Analyzing '$dirName' • [${++count}/$dirCount]...");
107 |
108 | occurrences.init(dirName);
109 | }
110 |
111 | void visitMethod(MethodInvocation node) {
112 | var libraryName = node.methodName.staticElement?.library?.name;
113 | if (libraryName?.startsWith('dart.') ?? false) {
114 | var typeName = node.realTarget?.staticType?.element?.name;
115 | var id = typeName ?? node.methodName.name;
116 | occurrences.add(dirName, library: libraryName!, symbol: id);
117 | }
118 | }
119 |
120 | void visitProperty(PropertyAccessorElement element) {
121 | var libraryName = element.library.name;
122 | if (libraryName.startsWith('dart.')) {
123 | if (element.variable is TopLevelVariableElement) {
124 | add(library: libraryName, symbol: element.name);
125 | }
126 | }
127 | }
128 |
129 | @override
130 | visitSimpleIdentifier(SimpleIdentifier node) {
131 | var parent = node.parent;
132 | if (parent is MethodInvocation) {
133 | visitMethod(parent);
134 | } else if (parent is NamedType) {
135 | visitTypeElement(parent.element);
136 | } else if (parent is PrefixedIdentifier) {
137 | var element = node.staticElement;
138 | if (element is ClassElement) {
139 | visitTypeElement(element);
140 | }
141 | } else {
142 | var element = node.staticElement;
143 | if (element is PropertyAccessorElement) {
144 | visitProperty(element);
145 | }
146 | }
147 | return super.visitSimpleIdentifier(node);
148 | }
149 |
150 | void visitType(NamedType type) {
151 | var element = type.element;
152 | if (element == null) return;
153 | var typeName = element.name;
154 | if (typeName == null) return;
155 | var libraryName = element.library?.name;
156 | if (libraryName?.startsWith('dart.') ?? false) {
157 | add(library: libraryName, symbol: typeName);
158 | }
159 | }
160 |
161 | void visitTypeElement(Element? element) {
162 | if (element == null) return;
163 |
164 | var typeName = element.name;
165 | if (typeName == null) return;
166 |
167 | var libraryName = element.library?.name;
168 | if (libraryName?.startsWith('dart.') ?? false) {
169 | add(library: libraryName, symbol: typeName);
170 | }
171 | }
172 | }
173 |
174 | class Occurrences {
175 | final Map>> data = {};
176 |
177 | Occurrences();
178 |
179 | factory Occurrences.fromFile(String path) =>
180 | Occurrences.fromJson(File(path).readAsStringSync());
181 |
182 | Occurrences.fromJson(String json) {
183 | Map decoded = jsonDecode(json);
184 | for (var e in decoded.entries) {
185 | var entries = (e.value as Map).map((key, value) => MapEntry(
186 | key as String, (value as List).map((e) => e as String).toList()));
187 | data[e.key] = entries;
188 | }
189 | }
190 |
191 | void add(String package, {required String library, required String symbol}) {
192 | data[package]!.update(library, (symbols) => symbols..addIfAbsent(symbol),
193 | ifAbsent: () => [symbol]);
194 | }
195 |
196 | void init(String package) {
197 | data[package] = {};
198 | }
199 |
200 | File toFile(String path) => File(path)
201 | ..createSync(recursive: true)
202 | ..writeAsStringSync(toJson());
203 |
204 | String toJson() => JsonEncoder.withIndent(' ').convert(data);
205 | }
206 |
207 | extension on List {
208 | void addIfAbsent(String value) {
209 | if (!contains(value)) add(value);
210 | }
211 | }
212 |
--------------------------------------------------------------------------------
/example/deprecated_api_surveyor.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2022 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import 'dart:io';
16 |
17 | import 'package:analyzer/dart/ast/ast.dart';
18 | import 'package:analyzer/dart/ast/visitor.dart';
19 | import 'package:analyzer/dart/element/element.dart';
20 | import 'package:analyzer/file_system/file_system.dart' hide File;
21 | import 'package:analyzer/source/line_info.dart';
22 | import 'package:path/path.dart' as path;
23 | import 'package:surveyor/src/driver.dart';
24 | import 'package:surveyor/src/visitors.dart';
25 |
26 | /// Looks for deprecated `dart:` library references.
27 | ///
28 | /// Run like so:
29 | ///
30 | /// dart run example/deprecated_api_surveyor.dart
31 | void main(List args) async {
32 | if (args.length == 1) {
33 | var dir = args[0];
34 | if (!File('$dir/pubspec.yaml').existsSync()) {
35 | print("Recursing into '$dir'...");
36 | args = Directory(dir).listSync().map((f) => f.path).toList()..sort();
37 | dirCount = args.length;
38 | print('(Found $dirCount subdirectories.)');
39 | }
40 | }
41 |
42 | if (_debugLimit != 0) {
43 | print('Limiting analysis to $_debugLimit packages.');
44 | }
45 |
46 | var driver = Driver.forArgs(args);
47 | driver.forceSkipInstall = true;
48 | driver.showErrors = false;
49 | driver.resolveUnits = true;
50 | driver.visitor = DeprecatedReferenceCollector();
51 |
52 | await driver.analyze(displayTiming: true, requirePackagesFile: false);
53 |
54 | print('# deprecated references: $deprecatedReferenceCount');
55 | print('# compilation units: $compilationUnitCount');
56 | print('');
57 | reports.forEach(print);
58 | }
59 |
60 | var compilationUnitCount = 0;
61 | var deprecatedReferenceCount = 0;
62 |
63 | int dirCount = 0;
64 |
65 | var reports = [];
66 |
67 | /// If non-zero, stops once limit is reached (for debugging).
68 | int _debugLimit = 0; //500;
69 |
70 | class DeprecatedReferenceCollector extends RecursiveAstVisitor
71 | implements PreAnalysisCallback, PostAnalysisCallback, AstContext {
72 | int count = 0;
73 | int contexts = 0;
74 | String? filePath;
75 | Folder? currentFolder;
76 | LineInfo? lineInfo;
77 |
78 | @override
79 | void postAnalysis(SurveyorContext context, DriverCommands cmd) {
80 | var debugLimit = _debugLimit;
81 | cmd.continueAnalyzing = debugLimit == 0 || count < debugLimit;
82 | }
83 |
84 | @override
85 | void preAnalysis(SurveyorContext context,
86 | {bool? subDir, DriverCommands? commandCallback}) {
87 | if (subDir ?? false) {
88 | ++dirCount;
89 | }
90 | var contextRoot = context.analysisContext.contextRoot;
91 | currentFolder = contextRoot.root;
92 | var dirName = path.basename(contextRoot.root.path);
93 |
94 | print("Analyzing '$dirName' • [${++count}/$dirCount]...");
95 | }
96 |
97 | @override
98 | void setFilePath(String filePath) {
99 | this.filePath = filePath;
100 | }
101 |
102 | @override
103 | void setLineInfo(LineInfo lineInfo) {
104 | this.lineInfo = lineInfo;
105 | }
106 |
107 | @override
108 | visitCompilationUnit(CompilationUnit node) {
109 | ++compilationUnitCount;
110 | super.visitCompilationUnit(node);
111 | }
112 |
113 | @override
114 | visitSimpleIdentifier(SimpleIdentifier node) {
115 | var element = node.staticElement;
116 | if (element == null) return;
117 | if (!_isDeprecated(element)) return;
118 | if (!_isInDartLib(element)) return;
119 |
120 | if (lineInfo != null) {
121 | var name = '';
122 | var parent = node.parent;
123 | if (parent is PrefixedIdentifier) {
124 | name = '${parent.prefix.name}.';
125 | }
126 | name += element.displayName;
127 |
128 | var location = lineInfo!.getLocation(node.offset);
129 | reports.add(
130 | '$name: $filePath:${location.lineNumber}:${location.columnNumber}');
131 | }
132 |
133 | ++deprecatedReferenceCount;
134 | }
135 |
136 | static bool _isDeprecated(Element element) {
137 | if (element is PropertyAccessorElement && element.isSynthetic) {
138 | return element.variable.hasDeprecated;
139 | }
140 | return element.hasDeprecated;
141 | }
142 |
143 | static bool _isInDartLib(Element element) {
144 | var name = element.library?.name;
145 | return name != null && name.startsWith('dart.');
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/example/doc_surveyor/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | include: package:lints/recommended.yaml
2 |
--------------------------------------------------------------------------------
/example/doc_surveyor/lib/main.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import 'src/doc_surveyor.dart';
16 |
17 | /// Example run:
18 | ///
19 | /// $ dart lib/main.dart /packages/provider
20 | /// 122 public members
21 | /// Members without docs:
22 | /// Void • /packages/provider/lib/src/proxy_provider.dart • 107:1
23 | /// NumericProxyProvider • /packages/provider/lib/src/proxy_provider.dart • 177:1
24 | /// Score: 0.98
25 | ///
26 | void main(List args) async {
27 | var stats = await analyzeDocs(args[0]);
28 | print('${stats.publicMemberCount} public members');
29 | print('Members without docs:');
30 | var locations = stats.undocumentedMemberLocations;
31 | for (var location in locations) {
32 | print(location.asString());
33 | }
34 |
35 | var score =
36 | ((stats.publicMemberCount - locations.length) / stats.publicMemberCount)
37 | .toStringAsFixed(2);
38 | print('Score: $score');
39 | }
40 |
--------------------------------------------------------------------------------
/example/doc_surveyor/lib/src/doc_surveyor.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | // ignore_for_file: implementation_imports
16 |
17 | import 'dart:io';
18 |
19 | import 'package:analyzer/dart/ast/ast.dart';
20 | import 'package:analyzer/dart/ast/visitor.dart';
21 | import 'package:analyzer/dart/element/element.dart';
22 | import 'package:analyzer/src/dart/element/inheritance_manager3.dart';
23 | import 'package:analyzer/src/dart/resolver/scope.dart';
24 | import 'package:analyzer/src/generated/source.dart';
25 | import 'package:path/path.dart' as path;
26 | import 'package:surveyor/src/analysis.dart';
27 | import 'package:surveyor/src/driver.dart';
28 | import 'package:surveyor/src/visitors.dart';
29 |
30 | Future analyzeDocs(String packageFolder, {bool silent = true}) async {
31 | var pubspec = path.join(packageFolder, 'pubspec.yaml');
32 | if (!File(pubspec).existsSync()) {
33 | throw Exception('File not found: $pubspec');
34 | }
35 |
36 | var driver = Driver.forArgs([packageFolder]);
37 | driver.forceSkipInstall = false;
38 | driver.excludedPaths = ['example', 'test'];
39 | driver.showErrors = true;
40 | driver.resolveUnits = true;
41 | driver.silent = silent;
42 | var visitor = _Visitor();
43 | driver.visitor = visitor;
44 |
45 | await driver.analyze();
46 |
47 | return visitor.stats;
48 | }
49 |
50 | class DocStats {
51 | int publicMemberCount = 0;
52 | final List undocumentedMemberLocations = [];
53 | }
54 |
55 | class SourceLocation {
56 | final String displayName;
57 | final Source source;
58 | final int line;
59 | final int column;
60 | SourceLocation(this.displayName, this.source, this.line, this.column);
61 |
62 | String get _bullet => !Platform.isWindows ? '•' : '-';
63 |
64 | String asString() =>
65 | '$displayName $_bullet ${source.fullName} $_bullet $line:$column';
66 | }
67 |
68 | class _Visitor extends RecursiveAstVisitor
69 | implements PreAnalysisCallback, AstContext {
70 | bool isInLibFolder = false;
71 |
72 | late InheritanceManager3 inheritanceManager;
73 |
74 | String? filePath;
75 |
76 | LineInfo? lineInfo;
77 |
78 | Set apiElements = {};
79 |
80 | DocStats stats = DocStats();
81 |
82 | _Visitor();
83 |
84 | bool check(Declaration node) {
85 | var lineInfo = this.lineInfo;
86 | if (lineInfo == null) return false;
87 |
88 | bool apiContains(Element? element) {
89 | while (element != null) {
90 | if (!element.isPrivate && apiElements.contains(element)) {
91 | return true;
92 | }
93 | element = element.enclosingElement;
94 | }
95 | return false;
96 | }
97 |
98 | if (!apiContains(node.declaredElement)) {
99 | return false;
100 | }
101 |
102 | stats.publicMemberCount++;
103 |
104 | if (node.documentationComment == null && !isOverridingMember(node)) {
105 | var location = lineInfo.getLocation(node.offset);
106 |
107 | var declaredElement = node.declaredElement;
108 | if (declaredElement != null) {
109 | var source = declaredElement.source;
110 | if (source != null) {
111 | stats.undocumentedMemberLocations.add(SourceLocation(
112 | declaredElement.displayName,
113 | source,
114 | location.lineNumber,
115 | location.columnNumber));
116 | return true;
117 | }
118 | }
119 | }
120 | return false;
121 | }
122 |
123 | Element? getOverriddenMember(Element? member) {
124 | if (member == null) {
125 | return null;
126 | }
127 |
128 | var classElement = member.thisOrAncestorOfType();
129 | if (classElement == null) {
130 | return null;
131 | }
132 | var libraryUri = classElement.library.source.uri;
133 | var memberName = member.name;
134 | if (memberName == null) {
135 | return null;
136 | }
137 | return inheritanceManager.getInherited(
138 | classElement.thisType,
139 | Name(libraryUri, memberName),
140 | );
141 | }
142 |
143 | bool isOverridingMember(Declaration node) =>
144 | getOverriddenMember(node.declaredElement) != null;
145 |
146 | @override
147 | void preAnalysis(SurveyorContext context,
148 | {bool? subDir, DriverCommands? commandCallback}) {
149 | inheritanceManager = InheritanceManager3();
150 | }
151 |
152 | @override
153 | void setFilePath(String filePath) {
154 | this.filePath = filePath;
155 | }
156 |
157 | @override
158 | void setLineInfo(LineInfo lineInfo) {
159 | this.lineInfo = lineInfo;
160 | }
161 |
162 | @override
163 | void visitClassDeclaration(ClassDeclaration node) {
164 | if (!isInLibFolder) return;
165 |
166 | if (isPrivate(node.name)) return;
167 |
168 | check(node);
169 |
170 | // Check methods
171 | var getters = {};
172 | var setters = [];
173 |
174 | // Non-getters/setters.
175 | var methods = [];
176 |
177 | // Identify getter/setter pairs.
178 | for (var member in node.members) {
179 | if (member is MethodDeclaration && !isPrivate(member.name)) {
180 | if (member.isGetter) {
181 | getters[member.name.lexeme] = member;
182 | } else if (member.isSetter) {
183 | setters.add(member);
184 | } else {
185 | methods.add(member);
186 | }
187 | }
188 | }
189 |
190 | // Check all getters, and collect offenders along the way.
191 | var missingDocs = {};
192 | for (var getter in getters.values) {
193 | if (check(getter)) {
194 | missingDocs.add(getter);
195 | }
196 | }
197 |
198 | // But only setters whose getter is missing a doc.
199 | for (var setter in setters) {
200 | var getter = getters[setter.name.lexeme];
201 | if (getter == null) {
202 | var declaredElement = node.declaredElement;
203 | if (declaredElement != null) {
204 | var libraryUri = declaredElement.library.source.uri;
205 | // Look for an inherited getter.
206 | var getter = inheritanceManager.getMember(
207 | declaredElement.thisType,
208 | Name(libraryUri, setter.name.lexeme),
209 | );
210 | if (getter is PropertyAccessorElement) {
211 | if (getter.documentationComment != null) {
212 | continue;
213 | }
214 | }
215 | check(setter);
216 | }
217 | } else if (missingDocs.contains(getter)) {
218 | check(setter);
219 | }
220 | }
221 |
222 | // Check remaining methods.
223 | methods.forEach(check);
224 | }
225 |
226 | @override
227 | void visitClassTypeAlias(ClassTypeAlias node) {
228 | if (!isInLibFolder) return;
229 |
230 | if (!isPrivate(node.name)) {
231 | check(node);
232 | }
233 | }
234 |
235 | @override
236 | void visitCompilationUnit(CompilationUnit node) {
237 | // Clear cached API elements.
238 | apiElements = {};
239 |
240 | var package = getPackage(node);
241 | if (package == null) return;
242 |
243 | // Ignore this compilation unit if it's not in the lib/ folder.
244 | isInLibFolder = isInLibDir(node, package);
245 | if (!isInLibFolder) return;
246 |
247 | var library = node.declaredElement?.library;
248 | if (library == null) return;
249 |
250 | var namespaceBuilder = NamespaceBuilder();
251 | var exports = namespaceBuilder.createExportNamespaceForLibrary(library);
252 | var public = namespaceBuilder.createPublicNamespaceForLibrary(library);
253 | apiElements.addAll(exports.definedNames.values);
254 | apiElements.addAll(public.definedNames.values);
255 |
256 | var getters = {};
257 | var setters = [];
258 |
259 | // Check functions.
260 |
261 | // Non-getters/setters.
262 | var functions = [];
263 |
264 | // Identify getter/setter pairs.
265 | for (var member in node.declarations) {
266 | if (member is FunctionDeclaration) {
267 | var name = member.name;
268 | if (!isPrivate(name) && name.lexeme != 'main') {
269 | if (member.isGetter) {
270 | getters[member.name.lexeme] = member;
271 | } else if (member.isSetter) {
272 | setters.add(member);
273 | } else {
274 | functions.add(member);
275 | }
276 | }
277 | }
278 | }
279 |
280 | // Check all getters, and collect offenders along the way.
281 | var missingDocs = {};
282 | for (var getter in getters.values) {
283 | if (check(getter)) {
284 | missingDocs.add(getter);
285 | }
286 | }
287 |
288 | // But only setters whose getter is missing a doc.
289 | for (var setter in setters) {
290 | var getter = getters[setter.name.lexeme];
291 | if (getter != null && missingDocs.contains(getter)) {
292 | check(setter);
293 | }
294 | }
295 |
296 | // Check remaining functions.
297 | functions.forEach(check);
298 |
299 | super.visitCompilationUnit(node);
300 | }
301 |
302 | @override
303 | void visitConstructorDeclaration(ConstructorDeclaration node) {
304 | if (!isInLibFolder) return;
305 |
306 | var nodeName = node.name;
307 | if (nodeName == null) return;
308 |
309 | if (!inPrivateMember(node) && !isPrivate(nodeName)) {
310 | check(node);
311 | }
312 | }
313 |
314 | @override
315 | void visitEnumConstantDeclaration(EnumConstantDeclaration node) {
316 | if (!isInLibFolder) return;
317 |
318 | if (!inPrivateMember(node) && !isPrivate(node.name)) {
319 | check(node);
320 | }
321 | }
322 |
323 | @override
324 | void visitEnumDeclaration(EnumDeclaration node) {
325 | if (!isInLibFolder) return;
326 |
327 | if (!isPrivate(node.name)) {
328 | check(node);
329 | }
330 | }
331 |
332 | @override
333 | void visitExtensionDeclaration(ExtensionDeclaration node) {
334 | if (!isInLibFolder) return;
335 |
336 | var nodeName = node.name;
337 | if (nodeName == null) return;
338 |
339 | if (node.name == null || isPrivate(nodeName)) {
340 | return;
341 | }
342 |
343 | check(node);
344 |
345 | // Check methods
346 |
347 | var getters = {};
348 | var setters = [];
349 |
350 | // Non-getters/setters.
351 | var methods = [];
352 |
353 | // Identify getter/setter pairs.
354 | for (var member in node.members) {
355 | if (member is MethodDeclaration && !isPrivate(member.name)) {
356 | if (member.isGetter) {
357 | getters[member.name.lexeme] = member;
358 | } else if (member.isSetter) {
359 | setters.add(member);
360 | } else {
361 | methods.add(member);
362 | }
363 | }
364 | }
365 |
366 | // Check all getters, and collect offenders along the way.
367 | var missingDocs = {};
368 | for (var getter in getters.values) {
369 | if (check(getter)) {
370 | missingDocs.add(getter);
371 | }
372 | }
373 |
374 | // But only setters whose getter is missing a doc.
375 | for (var setter in setters) {
376 | var getter = getters[setter.name.lexeme];
377 | if (getter != null && missingDocs.contains(getter)) {
378 | check(setter);
379 | }
380 | }
381 |
382 | // Check remaining methods.
383 | methods.forEach(check);
384 | }
385 |
386 | @override
387 | void visitFieldDeclaration(FieldDeclaration node) {
388 | if (!isInLibFolder) return;
389 |
390 | if (!inPrivateMember(node)) {
391 | for (var field in node.fields.variables) {
392 | if (!isPrivate(field.name)) {
393 | check(field);
394 | }
395 | }
396 | }
397 | }
398 |
399 | @override
400 | void visitFunctionTypeAlias(FunctionTypeAlias node) {
401 | if (!isInLibFolder) return;
402 |
403 | if (!isPrivate(node.name)) {
404 | check(node);
405 | }
406 | }
407 |
408 | @override
409 | void visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) {
410 | if (!isInLibFolder) return;
411 |
412 | for (var decl in node.variables.variables) {
413 | if (!isPrivate(decl.name)) {
414 | check(decl);
415 | }
416 | }
417 | }
418 | }
419 |
--------------------------------------------------------------------------------
/example/doc_surveyor/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: doc_surveyor
2 | description: Sample project that computes missing API doc stats.
3 |
4 | environment:
5 | sdk: '>=2.17.0 <3.0.0'
6 |
7 | dependencies:
8 | analyzer: ^5.4.0
9 | path: ^1.8.0
10 | surveyor:
11 | path: ../..
12 |
13 | dev_dependencies:
14 | lints: ^2.0.0
15 |
--------------------------------------------------------------------------------
/example/error_surveyor.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import 'dart:io';
16 |
17 | import 'package:analyzer/dart/analysis/results.dart';
18 | import 'package:analyzer/dart/ast/visitor.dart';
19 | import 'package:analyzer/error/error.dart';
20 | import 'package:analyzer/src/generated/engine.dart' show AnalysisErrorInfoImpl;
21 | import 'package:path/path.dart' as path;
22 | import 'package:surveyor/src/analysis.dart';
23 | import 'package:surveyor/src/driver.dart';
24 | import 'package:surveyor/src/visitors.dart';
25 |
26 | /// Analyzes projects, filtering specifically for errors of a specified type.
27 | ///
28 | /// Run like so:
29 | ///
30 | /// dart run example/error_surveyor.dart
31 | void main(List args) async {
32 | if (args.length == 1) {
33 | var dir = args[0];
34 | if (!File('$dir/pubspec.yaml').existsSync()) {
35 | print("Recursing into '$dir'...");
36 |
37 | args = Directory(dir)
38 | .listSync()
39 | .where(
40 | (f) => !path.basename(f.path).startsWith('.') && f is Directory)
41 | .map((f) => f.path)
42 | .toList()
43 | ..sort();
44 | print('(Found ${args.length} subdirectories.)');
45 | }
46 | dirCount = args.length;
47 | }
48 |
49 | if (_debugLimit != 0) {
50 | print('Limiting analysis to $_debugLimit packages.');
51 | }
52 |
53 | var driver = Driver.forArgs(args);
54 | driver.visitor = AnalysisAdvisor();
55 | driver.showErrors = true;
56 |
57 | // Uncomment to ignore test dirs.
58 | //driver.excludedPaths = ['test'];
59 |
60 | await driver.analyze(displayTiming: true);
61 | }
62 |
63 | int dirCount = 0;
64 |
65 | /// If non-zero, stops once limit is reached (for debugging).
66 | int _debugLimit = 0;
67 |
68 | class AnalysisAdvisor extends SimpleAstVisitor
69 | implements
70 | PreAnalysisCallback,
71 | PostAnalysisCallback,
72 | PostVisitCallback,
73 | ErrorReporter {
74 | int count = 0;
75 |
76 | final AnalysisStats stats;
77 | late final HumanErrorFormatter formatter;
78 |
79 | AnalysisAdvisor() : stats = AnalysisStats() {
80 | formatter = HumanErrorFormatter(stdout, stats);
81 | }
82 |
83 | @override
84 | void onVisitFinished() {
85 | stats.print();
86 | }
87 |
88 | @override
89 | void postAnalysis(SurveyorContext context, DriverCommands cmd) {
90 | var debugLimit = _debugLimit;
91 | cmd.continueAnalyzing = debugLimit == 0 || count < debugLimit;
92 | }
93 |
94 | @override
95 | void preAnalysis(SurveyorContext context,
96 | {bool? subDir, DriverCommands? commandCallback}) {
97 | if (subDir ?? false) {
98 | ++dirCount;
99 | }
100 | var root = context.analysisContext.contextRoot.root;
101 | var dirName = path.basename(root.path);
102 | if (subDir ?? false) {
103 | // Qualify.
104 | dirName = '${path.basename(root.parent.path)}/$dirName';
105 | }
106 | print("Analyzing '$dirName' • [${++count}/$dirCount]...");
107 | }
108 |
109 | @override
110 | void reportError(AnalysisResultWithErrors result) {
111 | var errors = result.errors.where(showError).toList();
112 | if (errors.isEmpty) {
113 | return;
114 | }
115 | formatter.formatErrors([AnalysisErrorInfoImpl(errors, result.lineInfo)]);
116 | formatter.flush();
117 | }
118 |
119 | bool showError(AnalysisError error) {
120 | var errorType = error.errorCode.type;
121 | if (errorType == ErrorType.HINT ||
122 | errorType == ErrorType.LINT ||
123 | errorType == ErrorType.TODO) {
124 | return false;
125 | }
126 | // todo (pq): filter on specific error type
127 | //print('${error.errorCode.type} : ${error.errorCode.name}');
128 | return true;
129 | }
130 | } // = 300;
131 |
--------------------------------------------------------------------------------
/example/lint_surveyor.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import 'dart:io';
16 |
17 | import 'package:analyzer/dart/analysis/results.dart';
18 | import 'package:analyzer/dart/ast/ast.dart';
19 | import 'package:analyzer/dart/ast/visitor.dart';
20 | import 'package:analyzer/error/error.dart';
21 | import 'package:analyzer/src/generated/engine.dart' show AnalysisErrorInfoImpl;
22 | import 'package:analyzer/src/lint/linter.dart';
23 | import 'package:path/path.dart' as path;
24 | import 'package:surveyor/src/analysis.dart';
25 | import 'package:surveyor/src/driver.dart';
26 | import 'package:surveyor/src/visitors.dart';
27 |
28 | /// Lints specified projects with a defined set of lints.
29 | ///
30 | /// Run like so:
31 | ///
32 | /// dart run example/lint_surveyor.dart
33 | void main(List args) async {
34 | if (args.length == 1) {
35 | var dir = args[0];
36 | if (!File('$dir/pubspec.yaml').existsSync()) {
37 | print("Recursing into '$dir'...");
38 |
39 | args = Directory(dir)
40 | .listSync()
41 | .where(
42 | (f) => !path.basename(f.path).startsWith('.') && f is Directory)
43 | .map((f) => f.path)
44 | .toList()
45 | ..sort();
46 | dirCount = args.length;
47 | print('(Found $dirCount subdirectories.)');
48 | }
49 | }
50 |
51 | if (_debugLimit != 0) {
52 | print('Limiting analysis to $_debugLimit packages.');
53 | }
54 |
55 | var driver = Driver.forArgs(args);
56 | driver.visitor = AnalysisAdvisor();
57 | driver.showErrors = true;
58 |
59 | driver.lints = [
60 | // Add a custom rule.
61 | CustomLint(),
62 | // And/or specify ones defined in the linter.
63 | // CamelCaseTypes(),
64 | ];
65 |
66 | await driver.analyze(displayTiming: true);
67 | }
68 |
69 | int dirCount = 0;
70 |
71 | /// If non-zero, stops once limit is reached (for debugging).
72 | int _debugLimit = 0; // = 300;
73 |
74 | class AnalysisAdvisor extends SimpleAstVisitor
75 | implements
76 | PreAnalysisCallback,
77 | PostAnalysisCallback,
78 | PostVisitCallback,
79 | ErrorReporter {
80 | int count = 0;
81 |
82 | AnalysisStats stats;
83 | late HumanErrorFormatter formatter;
84 |
85 | AnalysisAdvisor() : stats = AnalysisStats() {
86 | formatter = HumanErrorFormatter(stdout, stats);
87 | }
88 |
89 | @override
90 | void onVisitFinished() {
91 | stats.print();
92 | }
93 |
94 | @override
95 | void postAnalysis(SurveyorContext context, DriverCommands cmd) {
96 | var debugLimit = _debugLimit;
97 | cmd.continueAnalyzing = debugLimit == 0 || count < debugLimit;
98 | }
99 |
100 | @override
101 | void preAnalysis(SurveyorContext context,
102 | {bool? subDir, DriverCommands? commandCallback}) {
103 | subDir ??= false;
104 | if (subDir) {
105 | ++dirCount;
106 | }
107 | var root = context.analysisContext.contextRoot.root;
108 | var dirName = path.basename(root.path);
109 | if (subDir) {
110 | // Qualify.
111 | dirName = '${path.basename(root.parent.path)}/$dirName';
112 | }
113 | print("Analyzing '$dirName' • [${++count}/$dirCount]...");
114 | }
115 |
116 | @override
117 | void reportError(AnalysisResultWithErrors result) {
118 | var errors = result.errors.where(showError).toList();
119 | if (errors.isEmpty) {
120 | return;
121 | }
122 | formatter.formatErrors([AnalysisErrorInfoImpl(errors, result.lineInfo)]);
123 | formatter.flush();
124 | }
125 |
126 | //Only show lints.
127 | bool showError(AnalysisError error) => error.errorCode.type == ErrorType.LINT;
128 | }
129 |
130 | /// Sample content. Replace w/ custom logic.
131 | class CustomLint extends LintRule implements NodeLintRule {
132 | static const _desc = r'Avoid `print` calls in production code.';
133 | static const _details = r'''
134 | **DO** avoid `print` calls in production code.
135 |
136 | **BAD:**
137 | ```
138 | void f(int x) {
139 | print('debug: $x');
140 | ...
141 | }
142 | ```
143 | ''';
144 | CustomLint()
145 | : super(
146 | name: 'avoid_print',
147 | description: _desc,
148 | details: _details,
149 | group: Group.errors);
150 |
151 | @override
152 | void registerNodeProcessors(
153 | NodeLintRegistry registry, LinterContext context) {
154 | var visitor = _Visitor(this);
155 | registry.addMethodInvocation(this, visitor);
156 | }
157 | }
158 |
159 | class _Visitor extends SimpleAstVisitor {
160 | final LintRule rule;
161 |
162 | _Visitor(this.rule);
163 |
164 | @override
165 | void visitMethodInvocation(MethodInvocation node) {
166 | bool isDartCore(MethodInvocation node) =>
167 | node.methodName.staticElement?.library?.name == 'dart.core';
168 |
169 | if (node.methodName.name == 'print' && isDartCore(node)) {
170 | rule.reportLint(node.methodName);
171 | }
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/example/options_surveyor.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2022 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import 'dart:io';
16 |
17 | import 'package:analyzer/dart/ast/visitor.dart';
18 | import 'package:analyzer/src/lint/registry.dart'; // ignore: implementation_imports
19 | import 'package:path/path.dart' as path;
20 | import 'package:surveyor/src/common.dart';
21 | import 'package:surveyor/src/driver.dart';
22 | import 'package:surveyor/src/visitors.dart';
23 |
24 | /// Collects data about analysis options.
25 | ///
26 | /// Run like so:
27 | ///
28 | /// dart run example/options_surveyor.dart
29 | void main(List args) async {
30 | if (args.length == 1) {
31 | var dir = args[0];
32 | if (!File('$dir/pubspec.yaml').existsSync()) {
33 | print("Recursing into '$dir'...");
34 | args = Directory(dir).listSync().map((f) => f.path).toList()..sort();
35 | dirCount = args.length;
36 | print('(Found $dirCount subdirectories.)');
37 | }
38 | }
39 |
40 | if (_debugLimit != 0) {
41 | print('Limiting analysis to $_debugLimit packages.');
42 | }
43 |
44 | var driver = Driver.forArgs(args);
45 | //driver.forceSkipInstall = true;
46 | //driver.showErrors = true;
47 | //driver.resolveUnits = true;
48 | driver.visitor = OptionsVisitor();
49 |
50 | await driver.analyze(displayTiming: true);
51 |
52 | var optionsPercentage = (contextsWithOptions / count).toStringAsFixed(2);
53 | var lintsPercentage = (contextsWithLints / count).toStringAsFixed(2);
54 |
55 | var line = '----------------------------------------------------------------';
56 |
57 | print('');
58 | print(
59 | 'Contexts w/ options: $contextsWithOptions / $count • [ $optionsPercentage ]');
60 | print(
61 | 'Contexts w/ lints: $contextsWithLints / $count • [ $lintsPercentage ]');
62 | print('');
63 |
64 | print(line);
65 | print('LINT COUNTS: ---------------------------------------------------');
66 | print(line);
67 | printMap(lintCounts, width: line.length);
68 | print(line);
69 | print('INCLUDES: ------------------------------------------------------');
70 | print(line);
71 | printMap(includeCounts, width: line.length);
72 |
73 | print('(%s are relative to contexts w/ lints and then absolute count)');
74 | print('');
75 | print(line);
76 | print('LINTS w/ NO OCCURRENCES: ---------------------------------------');
77 | print(line);
78 | for (var rule in Registry.ruleRegistry.map((rule) => rule.name).toList()
79 | ..sort()) {
80 | if (!lintCounts.containsKey(rule)) {
81 | print(rule);
82 | }
83 | }
84 | }
85 |
86 | int contextsWithLints = 0;
87 | int contextsWithOptions = 0;
88 |
89 | int count = 0;
90 | int dirCount = 0;
91 |
92 | Map includeCounts = {};
93 | Map lintCounts = {};
94 |
95 | /// If non-zero, stops once limit is reached (for debugging).
96 | int _debugLimit = 0; //500;
97 |
98 | void printMap(Map map, {required int width}) {
99 | var entries = map.entries.toList()
100 | ..sort((e1, e2) => (e2.value - e1.value) * 100 + e1.key.compareTo(e2.key));
101 | for (var entry in entries) {
102 | var value = entry.value;
103 | var absolutePercent = (value / count).toStringAsFixed(2);
104 | var relativePercent = (value / contextsWithLints).toStringAsFixed(2);
105 | var percents = '[$relativePercent | $absolutePercent]';
106 | var valueCount = '${entry.key}: $value •';
107 | print('$valueCount ${percents.padLeft(width - (valueCount.length + 1))}');
108 | }
109 | print('');
110 | }
111 |
112 | class OptionsVisitor extends SimpleAstVisitor
113 | implements
114 | AnalysisOptionsVisitor,
115 | PreAnalysisCallback,
116 | PostAnalysisCallback {
117 | bool isExampleDir = false;
118 |
119 | @override
120 | void postAnalysis(SurveyorContext context, DriverCommands cmd) {
121 | if (isExampleDir) return;
122 |
123 | var debugLimit = _debugLimit;
124 | cmd.continueAnalyzing = debugLimit == 0 || count < debugLimit;
125 |
126 | var lintRules = context.analysisContext.analysisOptions.lintRules;
127 | if (lintRules.isNotEmpty) {
128 | ++contextsWithLints;
129 | for (var rule in lintRules) {
130 | lintCounts.increment(rule.name);
131 | }
132 | }
133 | }
134 |
135 | @override
136 | void preAnalysis(SurveyorContext context,
137 | {bool? subDir, DriverCommands? commandCallback}) {
138 | var contextRoot = context.analysisContext.contextRoot;
139 | var dirName = path.basename(contextRoot.root.path);
140 |
141 | isExampleDir = dirName == 'example';
142 | if (isExampleDir) return;
143 |
144 | if (subDir ?? false) {
145 | ++dirCount;
146 | }
147 | print("Analyzing '$dirName' • [${++count}/$dirCount]...");
148 | }
149 |
150 | @override
151 | void visit(AnalysisOptionsFile file) {
152 | if (isExampleDir) return;
153 |
154 | ++contextsWithOptions;
155 |
156 | var include = file.yaml['include'];
157 | if (include != null) {
158 | includeCounts.increment(include);
159 | }
160 | }
161 | }
162 |
163 | extension on Map {
164 | void increment(String key) =>
165 | update(key, (value) => value + 1, ifAbsent: () => 1);
166 | }
167 |
--------------------------------------------------------------------------------
/example/widget_surveyor/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | include: ../../analysis_options.yaml
2 |
3 | analyzer:
4 | exclude:
5 | - test/data/**
6 |
--------------------------------------------------------------------------------
/example/widget_surveyor/lib/widget_surveyor.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | // ignore_for_file: implementation_imports
16 |
17 | import 'dart:collection';
18 | import 'dart:convert';
19 | import 'dart:io';
20 |
21 | import 'package:analyzer/dart/ast/ast.dart';
22 | import 'package:analyzer/dart/ast/visitor.dart';
23 | import 'package:analyzer/dart/element/type.dart';
24 | import 'package:analyzer/src/generated/source.dart';
25 | import 'package:cli_util/cli_logging.dart';
26 | import 'package:corpus/corpus.dart';
27 | import 'package:path/path.dart' as path;
28 | import 'package:surveyor/src/analysis.dart';
29 | import 'package:surveyor/src/driver.dart';
30 | import 'package:surveyor/src/visitors.dart';
31 |
32 | /// Gathers and displays widget counts and 2-grams.
33 | ///
34 | /// Run like so:
35 | ///
36 | /// dart run example/widget_surveyor.dart
37 | ///
38 | /// Results are output in a file `results.json`. To get a summary
39 | /// of the results, pass `results.json` and optionally a corpus `index.json` as
40 | /// sole arguments to the surveyor:
41 | ///
42 | /// dart run example/widget_surveyor.dart results.json [index.json]
43 | ///
44 | /// (This will also produce a `results.csv` file and a `routes.csv` that can be
45 | /// used for further analysis.)
46 | ///
47 | void main(List args) async {
48 | var log = Logger.verbose();
49 | log.stdout('Surveying...');
50 |
51 | if (args.isNotEmpty) {
52 | if (args[0] == 'results.json') {
53 | // Disable tracing and timestamps.
54 | log = Logger.standard();
55 | log.stdout('Parsing results...');
56 | var results = ResultsReader().parse();
57 | var indexFile = checkForIndexFile(args);
58 | summarizeResults(results, indexFile!, log);
59 | return;
60 | }
61 | }
62 |
63 | var corpusDir = args[0];
64 | if (!File('$corpusDir/pubspec.yaml').existsSync()) {
65 | log.trace("Recursing into '$corpusDir'...");
66 | args = Directory(corpusDir).listSync().map((f) => f.path).toList();
67 | // for testing -- just analyze a few...
68 | //args = args.sublist(0, 3);
69 | }
70 |
71 | var collector = WidgetCollector(log, corpusDir);
72 |
73 | var driver = Driver.forArgs(args);
74 | driver.logger = log;
75 | driver.visitor = collector;
76 |
77 | await driver.analyze();
78 |
79 | log.stdout('Writing results.json...');
80 | var results =
81 | JsonEncoder.withIndent(' ').convert(collector.results.toJson());
82 | File('results.json').writeAsStringSync(results);
83 | log.stdout('Done');
84 | }
85 |
86 | IndexFile? checkForIndexFile(List args) {
87 | if (args.length == 2) {
88 | var filePath = args[1];
89 | if (path.basename(filePath) == 'index.json') {
90 | return IndexFile(filePath)..readSync();
91 | }
92 | }
93 | return null;
94 | }
95 |
96 | void summarizeResults(
97 | AnalysisResults results, IndexFile indexFile, Logger log) {
98 | var projectCount = 0;
99 | var skipCount = 0;
100 | var totals = {};
101 | for (var result in results) {
102 | var entries = result.widgetReferences.entries;
103 | if (entries.isNotEmpty) {
104 | ++projectCount;
105 | } else {
106 | ++skipCount;
107 | }
108 | // todo (pq): update to filter/flag test/example projects
109 | for (var referenceList in entries) {
110 | totals.update(
111 | referenceList.key,
112 | (v) => WidgetOccurrence(
113 | v.occurrences + referenceList.value.length, v.projects + 1),
114 | ifAbsent: () => WidgetOccurrence(referenceList.value.length, 1));
115 | }
116 | }
117 |
118 | log.stdout('Total projects: $projectCount ($skipCount skipped)');
119 | log.stdout('');
120 |
121 | var sorted = totals.entries.toList()
122 | ..sort((c1, c2) => c2.value.occurrences - c1.value.occurrences);
123 | String padClass(String s) => s.padRight(34);
124 | String padCount(String s) => s.padLeft(7);
125 | String padPercent(String s) => s.padLeft(21);
126 | log.stdout(
127 | '| ${padClass("class - (F)lutter")} | count | % containing projects |');
128 | log.stdout(
129 | '------------------------------------------------------------------------');
130 |
131 | for (var e in sorted) {
132 | var key = e.key;
133 | var inFlutter = key.startsWith('package:flutter/') ? ' (F)' : '';
134 | var name = '${key.split('#')[1]}$inFlutter';
135 | var count = e.value;
136 | var percent = (count.projects / projectCount).toStringAsFixed(2);
137 | log.stdout(
138 | '| ${padClass(name)} | ${padCount(count.occurrences.toString())} | ${padPercent(percent)} |');
139 | }
140 | log.stdout(
141 | '------------------------------------------------------------------------');
142 |
143 | CSVResultWriter(results).write();
144 | }
145 |
146 | class AnalysisResult {
147 | final String appName;
148 | final Map> widgetReferences;
149 |
150 | final int routeCount;
151 |
152 | AnalysisResult(this.appName, this.widgetReferences, this.routeCount);
153 |
154 | AnalysisResult.fromJson(Map json)
155 | : appName = json['name'],
156 | routeCount = json['routes'],
157 | widgetReferences = {} {
158 | var map = json['widgets'];
159 | for (var entry in map.entries) {
160 | widgetReferences[entry.key] = List.from(entry.value);
161 | }
162 | }
163 |
164 | Map toJson() =>
165 | {'name': appName, 'routes': routeCount, 'widgets': widgetReferences};
166 | }
167 |
168 | // bug: fixed in linter 0.1.116 (remove once landed)
169 | // ignore: prefer_mixin
170 | class AnalysisResults with IterableMixin {
171 | final List _results = [];
172 |
173 | AnalysisResults();
174 |
175 | AnalysisResults.fromJson(Map json) {
176 | var entries = json['details'];
177 | for (var entry in entries) {
178 | add(AnalysisResult.fromJson(entry));
179 | }
180 | }
181 |
182 | @override
183 | Iterator get iterator => _results.iterator;
184 |
185 | void add(AnalysisResult result) {
186 | _results.add(result);
187 | }
188 |
189 | Map toJson() => {
190 | // Summary?
191 | // ...
192 | // Details.
193 | 'details': [for (var result in _results) result.toJson()]
194 | };
195 | }
196 |
197 | class CSVResultWriter {
198 | final AnalysisResults results;
199 | CSVResultWriter(this.results);
200 |
201 | void write() {
202 | var resultSink = File('results.csv').openWrite();
203 | var routeSink = File('routes.csv').openWrite();
204 |
205 | for (var result in results) {
206 | for (var entry in result.widgetReferences.entries) {
207 | var references = entry.value;
208 | var widgetId = entry.key.replaceAll('#', ',');
209 | for (var ref in references) {
210 | resultSink.writeln('$widgetId,$ref');
211 | }
212 | }
213 | routeSink.writeln('${result.appName},${result.routeCount}');
214 | }
215 |
216 | resultSink.close();
217 | routeSink.close();
218 | }
219 | }
220 |
221 | class ResultsReader {
222 | AnalysisResults parse() {
223 | var json = jsonDecode(File('results.json').readAsStringSync());
224 | return AnalysisResults.fromJson(json);
225 | }
226 | }
227 |
228 | class TwoGram implements Comparable {
229 | final String parent;
230 | final String child;
231 |
232 | TwoGram(DartType parent, DartType child)
233 | : parent = parent.element?.name ?? 'null',
234 | child = child.element?.name ?? 'null';
235 |
236 | @override
237 | int get hashCode => parent.hashCode * 13 + child.hashCode;
238 |
239 | @override
240 | bool operator ==(other) =>
241 | other is TwoGram && other.child == child && other.parent == parent;
242 |
243 | @override
244 | int compareTo(TwoGram other) =>
245 | parent.compareTo(other.parent) * 2 + child.compareTo(other.child);
246 |
247 | @override
248 | String toString() => '$parent -> $child';
249 | }
250 |
251 | class TwoGrams {
252 | final Map map = {};
253 |
254 | void add(TwoGram twoGram) {
255 | map.update(twoGram, (v) => v + 1, ifAbsent: () => 1);
256 | }
257 |
258 | @override
259 | String toString() {
260 | var sb = StringBuffer();
261 | var entries = map.entries.toList()..sort((a, b) => a.key.compareTo(b.key));
262 | for (var entry in entries) {
263 | sb.writeln('${entry.key}, ${entry.value}');
264 | }
265 | return sb.toString();
266 | }
267 | }
268 |
269 | class WidgetCollector extends RecursiveAstVisitor
270 | implements AstContext, PreAnalysisCallback, PostAnalysisCallback {
271 | /// Sentinel to indicate we can't give a reliable route count.
272 | static const routesUnreliable = -1;
273 | final widgets = >{};
274 |
275 | var routes = 0;
276 | final results = AnalysisResults();
277 |
278 | final Logger log;
279 |
280 | late String dirName;
281 |
282 | late String filePath;
283 |
284 | late LineInfo lineInfo;
285 |
286 | final String corpusDir;
287 |
288 | WidgetCollector(this.log, this.corpusDir);
289 |
290 | String getLocation(InstanceCreationExpression node) {
291 | var file = path.relative(filePath, from: corpusDir);
292 | var location = lineInfo.getLocation(node.offset);
293 | return '$file:${location.lineNumber}:${location.columnNumber}';
294 | }
295 |
296 | String getSignature(DartType type) {
297 | Uri? uri;
298 |
299 | uri = type.element!.library!.source.uri;
300 | if (uri.isScheme('file')) {
301 | var converter = type.element!.library!.session.uriConverter;
302 | var path = converter.uriToPath(uri)!;
303 | uri = converter.pathToUri(path);
304 | }
305 |
306 | var name = type.element!.displayName;
307 | return '$uri#$name';
308 | }
309 |
310 | @override
311 | void postAnalysis(SurveyorContext context, DriverCommands _) {
312 | writeWidgetReferences();
313 | widgets.clear();
314 | routes = 0;
315 | }
316 |
317 | @override
318 | void preAnalysis(SurveyorContext context,
319 | {bool? subDir, DriverCommands? commandCallback}) {
320 | dirName = path.basename(context.analysisContext.contextRoot.root.path);
321 | log.stdout("Analyzing '$dirName'...");
322 | }
323 |
324 | @override
325 | void setFilePath(String filePath) {
326 | this.filePath = filePath;
327 | }
328 |
329 | @override
330 | void setLineInfo(LineInfo lineInfo) {
331 | this.lineInfo = lineInfo;
332 | }
333 |
334 | void updateRouteCount(DartType? type, InstanceCreationExpression node) {
335 | if (routes == routesUnreliable) {
336 | return;
337 | }
338 |
339 | if (implementsInterface(type, 'Route', '')) {
340 | ++routes;
341 | }
342 |
343 | if (implementsInterface(type, 'MaterialApp', '')) {
344 | var args = node.argumentList;
345 | for (var arg in args.arguments) {
346 | if (arg is NamedExpression) {
347 | if (arg.name.label.name == 'routes') {
348 | var exp = arg.expression;
349 | if (exp is SetOrMapLiteral) {
350 | routes += exp.elements.length;
351 | } else {
352 | // If it's not a map literal, we can't reasonably guess at a route
353 | // count so we flag it.
354 | routes = routesUnreliable;
355 | }
356 | }
357 | }
358 | }
359 | }
360 | }
361 |
362 | @override
363 | void visitInstanceCreationExpression(InstanceCreationExpression node) {
364 | var type = node.staticType;
365 |
366 | updateRouteCount(type, node);
367 |
368 | if (isWidgetType(type)) {
369 | var signature = getSignature(type!);
370 | var location = getLocation(node);
371 | widgets.update(signature, (v) => v..add(location),
372 | ifAbsent: () => [location]);
373 | }
374 |
375 | super.visitInstanceCreationExpression(node);
376 | }
377 |
378 | void writeWidgetReferences() {
379 | // var fileName = '${dirName}_widget.csv';
380 | // log.trace("Writing Widget counts to '${path.basename(fileName)}'...");
381 | // var sb = StringBuffer();
382 | // for (var entry in widgets.entries) {
383 | // var typeUri = entry.key;
384 | // var isFlutterWidget = typeUri.startsWith('package:flutter/');
385 | // var widgetType = isFlutterWidget ? 'flutter' : '*';
386 | // sb.writeln('$typeUri, ${entry.value}, $widgetType');
387 | // }
388 | // //TMP
389 | // print(sb.toString());
390 | // //File(fileName).writeAsStringSync(sb.toString());
391 |
392 | results.add(AnalysisResult(dirName, Map.from(widgets), routes));
393 | }
394 | }
395 |
396 | class WidgetOccurrence {
397 | int occurrences;
398 | int projects;
399 | WidgetOccurrence(this.occurrences, this.projects);
400 | }
401 |
--------------------------------------------------------------------------------
/example/widget_surveyor/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: widget_surveyor
2 | description: Sample project that performs widget surveying.
3 |
4 | environment:
5 | sdk: '>=2.17.0 <3.0.0'
6 |
7 | dependencies:
8 | analyzer: ^5.4.0
9 | cli_util: ^0.4.0
10 | corpus:
11 | # update when published
12 | git: https://github.com/pq/corpus
13 | path: ^1.8.0
14 | surveyor:
15 | path: ../..
16 |
17 | dev_dependencies:
18 | lints: ^2.0.0
19 | test: ^1.14.0
20 |
--------------------------------------------------------------------------------
/example/widget_surveyor/test/data/animated_container/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | # Defines a default set of lint rules enforced for
2 | # projects at Google. For details and rationale,
3 | # see https://github.com/dart-lang/pedantic#enabled-lints.
4 | include: package:pedantic/analysis_options.yaml
5 |
--------------------------------------------------------------------------------
/example/widget_surveyor/test/data/animated_container/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'dart:math';
2 |
3 | import 'package:flutter/material.dart';
4 |
5 | void main() => runApp(AnimatedContainerApp());
6 |
7 | class AnimatedContainerApp extends StatefulWidget {
8 | @override
9 | _AnimatedContainerAppState createState() => _AnimatedContainerAppState();
10 | }
11 |
12 | class _AnimatedContainerAppState extends State {
13 | // Define the various properties with default values. Update these properties
14 | // when the user taps a FloatingActionButton.
15 | double _width = 50;
16 | double _height = 50;
17 | Color _color = Colors.green;
18 | BorderRadiusGeometry _borderRadius = BorderRadius.circular(8);
19 |
20 | @override
21 | Widget build(BuildContext context) {
22 | return MaterialApp(
23 | home: Scaffold(
24 | appBar: AppBar(
25 | title: Text('AnimatedContainer Demo'),
26 | ),
27 | body: Center(
28 | child: AnimatedContainer(
29 | // Use the properties stored in the State class.
30 | width: _width,
31 | height: _height,
32 | decoration: BoxDecoration(
33 | color: _color,
34 | borderRadius: _borderRadius,
35 | ),
36 | // Define how long the animation should take.
37 | duration: Duration(seconds: 1),
38 | // Provide an optional curve to make the animation feel smoother.
39 | curve: Curves.fastOutSlowIn,
40 | ),
41 | ),
42 | floatingActionButton: FloatingActionButton(
43 | child: Icon(Icons.play_arrow),
44 | // When the user taps the button
45 | onPressed: () {
46 | // Use setState to rebuild the widget with new values.
47 | setState(() {
48 | // Create a random number generator.
49 | final random = Random();
50 |
51 | // Generate a random width and height.
52 | _width = random.nextInt(300).toDouble();
53 | _height = random.nextInt(300).toDouble();
54 |
55 | // Generate a random color.
56 | _color = Color.fromRGBO(
57 | random.nextInt(256),
58 | random.nextInt(256),
59 | random.nextInt(256),
60 | 1,
61 | );
62 |
63 | // Generate a random border radius.
64 | _borderRadius =
65 | BorderRadius.circular(random.nextInt(100).toDouble());
66 | });
67 | },
68 | ),
69 | ),
70 | );
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/example/widget_surveyor/test/data/animated_container/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: animated_container
2 | description: A new Flutter project.
3 |
4 | version: 1.0.0+1
5 |
6 | environment:
7 | sdk: ">=2.0.0-dev.68.0 <3.0.0"
8 |
9 | dependencies:
10 | flutter:
11 | sdk: flutter
12 | cupertino_icons: ^0.1.2
13 |
14 | dev_dependencies:
15 | flutter_test:
16 | sdk: flutter
17 |
18 | flutter:
19 | uses-material-design: true
20 |
21 | # To add assets to your application, add an assets section, like this:
22 | # assets:
23 | # - images/a_dot_burr.jpeg
24 | # - images/a_dot_ham.jpeg
25 |
26 | # An image asset can refer to one or more resolution-specific "variants", see
27 | # https://flutter.io/assets-and-images/#resolution-aware.
28 |
29 | # For details regarding adding assets from package dependencies, see
30 | # https://flutter.io/assets-and-images/#from-packages
31 |
32 | # To add custom fonts to your application, add a fonts section here,
33 | # in this "flutter" section. Each entry in this list should have a
34 | # "family" key with the font family name, and a "fonts" key with a
35 | # list giving the asset and other descriptors for the font. For
36 | # example:
37 | # fonts:
38 | # - family: Schyler
39 | # fonts:
40 | # - asset: fonts/Schyler-Regular.ttf
41 | # - asset: fonts/Schyler-Italic.ttf
42 | # style: italic
43 | # - family: Trajan Pro
44 | # fonts:
45 | # - asset: fonts/TrajanPro.ttf
46 | # - asset: fonts/TrajanPro_Bold.ttf
47 | # weight: 700
48 | #
49 | # For details regarding fonts from package dependencies,
50 | # see https://flutter.io/custom-fonts/#from-packages
51 |
--------------------------------------------------------------------------------
/example/widget_surveyor/test/data/async/lib/main.dart:
--------------------------------------------------------------------------------
1 | main() async {}
2 |
3 | var async = 0;
4 |
--------------------------------------------------------------------------------
/example/widget_surveyor/test/data/async/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: async
2 | description: An app that uses async.
3 |
4 | version: 1.0.0+1
5 |
6 | environment:
7 | sdk: ">=2.0.0-dev.68.0 <3.0.0"
8 |
--------------------------------------------------------------------------------
/example/widget_surveyor/test/data/basic_app/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | # Defines a default set of lint rules enforced for
2 | # projects at Google. For details and rationale,
3 | # see https://github.com/dart-lang/pedantic#enabled-lints.
4 | include: package:pedantic/analysis_options.yaml
5 |
--------------------------------------------------------------------------------
/example/widget_surveyor/test/data/basic_app/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | void main() => runApp(MyApp());
4 |
5 | class MyApp extends MaterialApp {
6 | Widget build(BuildContext context) {
7 | return new MaterialApp(
8 | title: 'Flutter Demo',
9 | theme: ThemeData(
10 | primarySwatch: Colors.blue,
11 | ),
12 | home: MyHomePage(title: 'Flutter Demo Home Page'),
13 | );
14 | }
15 | }
16 |
17 | class MyHomePage extends StatefulWidget {
18 | MyHomePage({Key key, this.title}) : super(key: key);
19 |
20 | final String title;
21 |
22 | @override
23 | _MyHomePageState createState() => new _MyHomePageState();
24 | }
25 |
26 | class _MyHomePageState extends State {
27 | int _counter = 0;
28 |
29 | void _incrementCounter() {
30 | setState(() {
31 | _counter++;
32 | });
33 | }
34 |
35 | @override
36 | Widget build(BuildContext context) {
37 | return Scaffold(
38 | appBar: AppBar(
39 | title: Text(widget.title),
40 | ),
41 | body: Center(
42 | child: Column(
43 | mainAxisAlignment: MainAxisAlignment.center,
44 | children: [
45 | Text(
46 | 'Pushed',
47 | ),
48 | Text(
49 | '$_counter',
50 | style: Theme.of(context).textTheme.display1,
51 | ),
52 | Text(
53 | 'times',
54 | ),
55 | ],
56 | ),
57 | ),
58 | floatingActionButton: new FloatingActionButton(
59 | onPressed: _incrementCounter,
60 | tooltip: 'Increment',
61 | child: Icon(Icons.add),
62 | ),
63 | );
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/example/widget_surveyor/test/data/basic_app/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: basic_app
2 | description: A new Flutter project.
3 |
4 | version: 1.0.0+1
5 |
6 | environment:
7 | sdk: ">=2.0.0-dev.68.0 <3.0.0"
8 |
9 | dependencies:
10 | flutter:
11 | sdk: flutter
12 | cupertino_icons: ^0.1.2
13 |
14 | dev_dependencies:
15 | flutter_test:
16 | sdk: flutter
17 |
18 | flutter:
19 | uses-material-design: true
20 |
21 | # To add assets to your application, add an assets section, like this:
22 | # assets:
23 | # - images/a_dot_burr.jpeg
24 | # - images/a_dot_ham.jpeg
25 |
26 | # An image asset can refer to one or more resolution-specific "variants", see
27 | # https://flutter.io/assets-and-images/#resolution-aware.
28 |
29 | # For details regarding adding assets from package dependencies, see
30 | # https://flutter.io/assets-and-images/#from-packages
31 |
32 | # To add custom fonts to your application, add a fonts section here,
33 | # in this "flutter" section. Each entry in this list should have a
34 | # "family" key with the font family name, and a "fonts" key with a
35 | # list giving the asset and other descriptors for the font. For
36 | # example:
37 | # fonts:
38 | # - family: Schyler
39 | # fonts:
40 | # - asset: fonts/Schyler-Regular.ttf
41 | # - asset: fonts/Schyler-Italic.ttf
42 | # style: italic
43 | # - family: Trajan Pro
44 | # fonts:
45 | # - asset: fonts/TrajanPro.ttf
46 | # - asset: fonts/TrajanPro_Bold.ttf
47 | # weight: 700
48 | #
49 | # For details regarding fonts from package dependencies,
50 | # see https://flutter.io/custom-fonts/#from-packages
51 |
--------------------------------------------------------------------------------
/example/widget_surveyor/test/data/basic_app/test/has_error.dart:
--------------------------------------------------------------------------------
1 | int x = '';
2 |
--------------------------------------------------------------------------------
/example/widget_surveyor/test/data/basic_app/test/main_test.dart:
--------------------------------------------------------------------------------
1 | // Place holder.
2 |
--------------------------------------------------------------------------------
/example/widget_surveyor/test/data/route_app/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | # Defines a default set of lint rules enforced for
2 | # projects at Google. For details and rationale,
3 | # see https://github.com/dart-lang/pedantic#enabled-lints.
4 | include: package:pedantic/analysis_options.yaml
5 |
--------------------------------------------------------------------------------
/example/widget_surveyor/test/data/route_app/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | void main() {
4 | runApp(MaterialApp(
5 | title: 'Named Routes Demo',
6 | // Start the app with the "/" named route. In this case, the app starts
7 | // on the FirstScreen widget.
8 | initialRoute: '/',
9 | routes: {
10 | // When navigating to the "/" route, build the FirstScreen widget.
11 | '/': (context) => FirstScreen(),
12 | // When navigating to the "/second" route, build the SecondScreen widget.
13 | '/second': (context) => SecondScreen(),
14 | },
15 | ));
16 | }
17 |
18 | class FirstScreen extends StatelessWidget {
19 | @override
20 | Widget build(BuildContext context) {
21 | return Scaffold(
22 | appBar: AppBar(
23 | title: Text('First Screen'),
24 | ),
25 | body: Center(
26 | child: RaisedButton(
27 | child: Text('Launch screen'),
28 | onPressed: () {
29 | Navigator.pushNamed(context, '/second');
30 | },
31 | ),
32 | ),
33 | );
34 | }
35 | }
36 |
37 | class SecondScreen extends StatelessWidget {
38 | @override
39 | Widget build(BuildContext context) {
40 | return Scaffold(
41 | appBar: AppBar(
42 | title: Text("Second Screen"),
43 | ),
44 | body: Center(
45 | child: RaisedButton(
46 | onPressed: () {
47 | Navigator.pop(context);
48 | },
49 | child: Text('Go back!'),
50 | ),
51 | ),
52 | );
53 | }
54 | }
55 |
56 | void main2() {
57 | runApp(MaterialApp(
58 | title: 'Navigation Basics',
59 | home: FirstRoute(),
60 | ));
61 | }
62 |
63 | class FirstRoute extends StatelessWidget {
64 | @override
65 | Widget build(BuildContext context) {
66 | return Scaffold(
67 | appBar: AppBar(
68 | title: Text('First Route'),
69 | ),
70 | body: Center(
71 | child: RaisedButton(
72 | child: Text('Open route'),
73 | onPressed: () {
74 | Navigator.push(
75 | context,
76 | MaterialPageRoute(builder: (context) => SecondRoute()),
77 | );
78 | },
79 | ),
80 | ),
81 | );
82 | }
83 | }
84 |
85 | class SecondRoute extends StatelessWidget {
86 | @override
87 | Widget build(BuildContext context) {
88 | return Scaffold(
89 | appBar: AppBar(
90 | title: Text("Second Route"),
91 | ),
92 | body: Center(
93 | child: RaisedButton(
94 | onPressed: () {
95 | Navigator.pop(context);
96 | },
97 | child: Text('Go back!'),
98 | ),
99 | ),
100 | );
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/example/widget_surveyor/test/data/route_app/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: route_app
2 | description: A new Flutter project.
3 |
4 | version: 1.0.0+1
5 |
6 | environment:
7 | sdk: ">=2.0.0-dev.68.0 <3.0.0"
8 |
9 | dependencies:
10 | flutter:
11 | sdk: flutter
12 | cupertino_icons: ^0.1.2
13 |
14 | dev_dependencies:
15 | flutter_test:
16 | sdk: flutter
17 |
18 | flutter:
19 | uses-material-design: true
20 |
21 | # To add assets to your application, add an assets section, like this:
22 | # assets:
23 | # - images/a_dot_burr.jpeg
24 | # - images/a_dot_ham.jpeg
25 |
26 | # An image asset can refer to one or more resolution-specific "variants", see
27 | # https://flutter.io/assets-and-images/#resolution-aware.
28 |
29 | # For details regarding adding assets from package dependencies, see
30 | # https://flutter.io/assets-and-images/#from-packages
31 |
32 | # To add custom fonts to your application, add a fonts section here,
33 | # in this "flutter" section. Each entry in this list should have a
34 | # "family" key with the font family name, and a "fonts" key with a
35 | # list giving the asset and other descriptors for the font. For
36 | # example:
37 | # fonts:
38 | # - family: Schyler
39 | # fonts:
40 | # - asset: fonts/Schyler-Regular.ttf
41 | # - asset: fonts/Schyler-Italic.ttf
42 | # style: italic
43 | # - family: Trajan Pro
44 | # fonts:
45 | # - asset: fonts/TrajanPro.ttf
46 | # - asset: fonts/TrajanPro_Bold.ttf
47 | # weight: 700
48 | #
49 | # For details regarding fonts from package dependencies,
50 | # see https://flutter.io/custom-fonts/#from-packages
51 |
--------------------------------------------------------------------------------
/example/widget_surveyor/test/widget_surveyor_test.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import 'package:cli_util/cli_logging.dart';
16 | import 'package:surveyor/src/driver.dart';
17 | import 'package:test/test.dart';
18 | import 'package:widget_surveyor/widget_surveyor.dart';
19 |
20 | // import '..widget_surveyor.dart';
21 |
22 | Future main() async {
23 | group('widget survey', () {
24 | test('basic_app', () async {
25 | var result = await analyze('test/data/basic_app');
26 | var refs = result.widgetReferences;
27 | expectContains(
28 | refs,
29 | {
30 | 'package:basic_app/main.dart#MyApp': ['lib/main.dart:3:23'],
31 | 'package:flutter/src/material/app.dart#MaterialApp': [
32 | 'lib/main.dart:8:12'
33 | ],
34 | 'package:basic_app/main.dart#MyHomePage': ['lib/main.dart:13:13'],
35 | 'package:flutter/src/material/scaffold.dart#Scaffold': [
36 | 'lib/main.dart:38:13'
37 | ],
38 | 'package:flutter/src/material/app_bar.dart#AppBar': [
39 | 'lib/main.dart:39:15'
40 | ],
41 | 'package:flutter/src/widgets/text.dart#Text': [
42 | 'lib/main.dart:40:16',
43 | 'lib/main.dart:46:13',
44 | 'lib/main.dart:49:13',
45 | 'lib/main.dart:53:13'
46 | ],
47 | 'package:flutter/src/widgets/basic.dart#Center': [
48 | 'lib/main.dart:42:13'
49 | ],
50 | 'package:flutter/src/widgets/basic.dart#Column': [
51 | 'lib/main.dart:43:16'
52 | ],
53 | 'package:flutter/src/material/floating_action_button.dart#FloatingActionButton':
54 | ['lib/main.dart:59:29'],
55 | 'package:flutter/src/widgets/icon.dart#Icon': ['lib/main.dart:62:16']
56 | },
57 | );
58 | expect(0, result.routeCount);
59 | });
60 | test('route_app', () async {
61 | var result = await analyze('test/data/route_app');
62 | var refs = result.widgetReferences;
63 | expectContains(
64 | refs,
65 | {
66 | 'package:flutter/src/material/app.dart#MaterialApp': [
67 | 'lib/main.dart:4:10',
68 | 'lib/main.dart:58:10'
69 | ],
70 | 'package:route_app/main.dart#FirstScreen': ['lib/main.dart:11:25'],
71 | 'package:route_app/main.dart#SecondScreen': ['lib/main.dart:13:31'],
72 | 'package:flutter/src/material/scaffold.dart#Scaffold': [
73 | 'lib/main.dart:21:12',
74 | 'lib/main.dart:40:12',
75 | 'lib/main.dart:67:12',
76 | 'lib/main.dart:89:12'
77 | ],
78 | 'package:flutter/src/material/app_bar.dart#AppBar': [
79 | 'lib/main.dart:22:15',
80 | 'lib/main.dart:41:15',
81 | 'lib/main.dart:68:15',
82 | 'lib/main.dart:90:15'
83 | ],
84 | 'package:flutter/src/widgets/text.dart#Text': [
85 | 'lib/main.dart:23:16',
86 | 'lib/main.dart:27:18',
87 | 'lib/main.dart:42:16',
88 | 'lib/main.dart:49:18',
89 | 'lib/main.dart:69:16',
90 | 'lib/main.dart:73:18',
91 | 'lib/main.dart:91:16',
92 | 'lib/main.dart:98:18'
93 | ],
94 | 'package:flutter/src/widgets/basic.dart#Center': [
95 | 'lib/main.dart:25:13',
96 | 'lib/main.dart:44:13',
97 | 'lib/main.dart:71:13',
98 | 'lib/main.dart:93:13'
99 | ],
100 | 'package:flutter/src/material/raised_button.dart#RaisedButton': [
101 | 'lib/main.dart:26:16',
102 | 'lib/main.dart:45:16',
103 | 'lib/main.dart:72:16',
104 | 'lib/main.dart:94:16'
105 | ],
106 | 'package:route_app/main.dart#FirstRoute': ['lib/main.dart:60:11'],
107 | 'package:route_app/main.dart#SecondRoute': ['lib/main.dart:77:55']
108 | },
109 | );
110 | expect(3, result.routeCount);
111 | });
112 | });
113 | }
114 |
115 | Future analyze(String path, {Logger? log}) async {
116 | var driver = Driver.forArgs([path]);
117 | var collector = WidgetCollector(log ?? Logger.standard(), path);
118 | driver.visitor = collector;
119 | await driver.analyze();
120 | var results = collector.results.toList();
121 | expect(results, hasLength(1));
122 | return results[0];
123 | }
124 |
125 | void expectContains(
126 | Map> m1, Map> m2) {
127 | for (var e in m2.entries) {
128 | expect(m1, containsPair(e.key, e.value));
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/lib/src/analysis.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import 'dart:io' as io;
16 |
17 | import 'package:analyzer/dart/ast/ast.dart';
18 | import 'package:analyzer/dart/ast/token.dart';
19 | import 'package:analyzer/dart/element/type.dart';
20 | import 'package:analyzer/error/error.dart';
21 | import 'package:analyzer/source/line_info.dart';
22 | import 'package:analyzer/src/generated/engine.dart'; // ignore: implementation_imports
23 | import 'package:analyzer/src/generated/source.dart'; // ignore: implementation_imports
24 | import 'package:analyzer/src/workspace/workspace.dart'; // ignore: implementation_imports
25 | import 'package:path/path.dart' as path;
26 |
27 | final Map _severityCompare = {
28 | 'error': 5,
29 | 'warning': 4,
30 | 'info': 3,
31 | 'lint': 2,
32 | 'hint': 1,
33 | };
34 |
35 | WorkspacePackage? getPackage(CompilationUnit unit) {
36 | var element = unit.declaredElement;
37 | if (element == null) {
38 | return null;
39 | }
40 | var libraryPath = element.library.source.fullName;
41 | var workspace = element.session.analysisContext.contextRoot.workspace;
42 | return workspace.findPackageFor(libraryPath);
43 | }
44 |
45 | bool implementsInterface(DartType? type, String interface, String library) {
46 | if (type is! InterfaceType) {
47 | return false;
48 | }
49 | bool predicate(InterfaceType i) => isInterface(i, interface, library);
50 | var element = type.element;
51 | return predicate(type) ||
52 | !element.isSynthetic && element.allSupertypes.any(predicate);
53 | }
54 |
55 | /// Returns `true` if this [node] is the child of a private compilation unit
56 | /// member.
57 | bool inPrivateMember(AstNode node) {
58 | var parent = node.parent;
59 | if (parent is NamedCompilationUnitMember) {
60 | return Identifier.isPrivateName(parent.name.lexeme);
61 | }
62 | if (parent is ExtensionDeclaration) {
63 | var parentName = parent.name;
64 | return parentName == null || Identifier.isPrivateName(parentName.lexeme);
65 | }
66 | return false;
67 | }
68 |
69 | /// Return true if this compilation unit [node] is declared within the given
70 | /// [package]'s `lib/` directory tree.
71 | bool isInLibDir(CompilationUnit node, WorkspacePackage package) {
72 | var cuPath = node.declaredElement?.library.source.fullName;
73 | if (cuPath == null) return false;
74 | var libDir = path.join(package.root, 'lib');
75 | return path.isWithin(libDir, cuPath);
76 | }
77 |
78 | bool isInterface(InterfaceType type, String interface, String library) =>
79 | type.element.name == interface && type.element.library.name == library;
80 |
81 | /// Check if the given token has a private name.
82 | bool isPrivate(Token token) => Identifier.isPrivateName(token.lexeme);
83 |
84 | bool isWidgetType(DartType? type) => implementsInterface(type, 'Widget', '');
85 |
86 | String _pluralize(String word, int count) => count == 1 ? word : '${word}s';
87 |
88 | /// Given an absolute path, return a relative path if the file is contained in
89 | /// the current directory; return the original path otherwise.
90 | String _relative(String file) =>
91 | file.startsWith(path.current) ? path.relative(file) : file;
92 |
93 | /// Returns the given error's severity.
94 | ErrorSeverity _severityIdentity(AnalysisError error) =>
95 | error.errorCode.errorSeverity;
96 |
97 | /// Returns desired severity for the given [error] (or `null` if it's to be
98 | /// suppressed).
99 | typedef SeverityProcessor = ErrorSeverity Function(AnalysisError error);
100 |
101 | /// Analysis statistics counter.
102 | class AnalysisStats {
103 | int unfilteredCount = 0;
104 |
105 | int errorCount = 0;
106 | int hintCount = 0;
107 | int lintCount = 0;
108 | int warnCount = 0;
109 |
110 | AnalysisStats();
111 |
112 | /// The total number of diagnostics reported to the user.
113 | int get filteredCount => errorCount + warnCount + hintCount + lintCount;
114 |
115 | /// Print statistics to [out].
116 | void print([StringSink? out]) {
117 | out ??= io.stdout;
118 | var hasErrors = errorCount != 0;
119 | var hasWarns = warnCount != 0;
120 | var hasHints = hintCount != 0;
121 | var hasLints = lintCount != 0;
122 | var hasContent = false;
123 | if (hasErrors) {
124 | out.write(errorCount);
125 | out.write(' ');
126 | out.write(_pluralize('error', errorCount));
127 | hasContent = true;
128 | }
129 | if (hasWarns) {
130 | if (hasContent) {
131 | if (!hasHints && !hasLints) {
132 | out.write(' and ');
133 | } else {
134 | out.write(', ');
135 | }
136 | }
137 | out.write(warnCount);
138 | out.write(' ');
139 | out.write(_pluralize('warning', warnCount));
140 | hasContent = true;
141 | }
142 | if (hasLints) {
143 | if (hasContent) {
144 | out.write(hasHints ? ', ' : ' and ');
145 | }
146 | out.write(lintCount);
147 | out.write(' ');
148 | out.write(_pluralize('lint', lintCount));
149 | hasContent = true;
150 | }
151 | if (hasHints) {
152 | if (hasContent) {
153 | out.write(' and ');
154 | }
155 | out.write(hintCount);
156 | out.write(' ');
157 | out.write(_pluralize('hint', hintCount));
158 | hasContent = true;
159 | }
160 | if (hasContent) {
161 | out.writeln(' found.');
162 | } else {
163 | out.writeln('No issues found!');
164 | }
165 | }
166 | }
167 |
168 | class AnsiLogger {
169 | bool useAnsi;
170 |
171 | AnsiLogger(this.useAnsi);
172 | String get blue => _code('\u001b[34m');
173 | String get bold => _code('\u001b[1m');
174 | String get bullet => !io.Platform.isWindows ? '•' : '-';
175 | String get cyan => _code('\u001b[36m');
176 | String get gray => _code('\u001b[1;30m');
177 | String get green => _code('\u001b[32m');
178 | String get magenta => _code('\u001b[35m');
179 | String get noColor => _code('\u001b[39m');
180 | String get none => _code('\u001b[0m');
181 |
182 | String get red => _code('\u001b[31m');
183 |
184 | String get yellow => _code('\u001b[33m');
185 |
186 | String _code(String ansiCode) => useAnsi ? ansiCode : '';
187 | }
188 |
189 | /// An [AnalysisError] with line and column information.
190 | class CLIError implements Comparable {
191 | String severity;
192 | String sourcePath;
193 | int offset;
194 | int line;
195 | int column;
196 | String message;
197 | String errorCode;
198 | String? correction;
199 |
200 | CLIError({
201 | required this.severity,
202 | required this.sourcePath,
203 | required this.offset,
204 | required this.line,
205 | required this.column,
206 | required this.message,
207 | required this.errorCode,
208 | required this.correction,
209 | });
210 |
211 | @override
212 | int get hashCode =>
213 | severity.hashCode ^ sourcePath.hashCode ^ errorCode.hashCode ^ offset;
214 | bool get isError => severity == 'error';
215 | bool get isHint => severity == 'hint';
216 | bool get isLint => severity == 'lint';
217 |
218 | bool get isWarning => severity == 'warning';
219 |
220 | @override
221 | bool operator ==(other) {
222 | if (other is! CLIError) return false;
223 |
224 | return severity == other.severity &&
225 | sourcePath == other.sourcePath &&
226 | errorCode == other.errorCode &&
227 | offset == other.offset;
228 | }
229 |
230 | @override
231 | int compareTo(CLIError other) {
232 | // severity
233 | var compare =
234 | _severityCompare[other.severity]! - _severityCompare[severity]!;
235 | if (compare != 0) return compare;
236 |
237 | // path
238 | compare = Comparable.compare(
239 | sourcePath.toLowerCase(), other.sourcePath.toLowerCase());
240 | if (compare != 0) return compare;
241 |
242 | // offset
243 | return offset - other.offset;
244 | }
245 | }
246 |
247 | /// Helper for formatting [AnalysisError]s.
248 | ///
249 | /// The two format options are a user consumable format and a machine consumable
250 | /// format.
251 | abstract class ErrorFormatter {
252 | StringSink out;
253 | AnalysisStats stats;
254 | final SeverityProcessor _severityProcessor;
255 |
256 | ErrorFormatter(this.out, this.stats, {SeverityProcessor? severityProcessor})
257 | : _severityProcessor = severityProcessor ?? _severityIdentity;
258 |
259 | /// Call to write any batched up errors from [formatErrors].
260 | void flush();
261 |
262 | void formatError(
263 | Map errorToLine, AnalysisError error);
264 |
265 | void formatErrors(List errorInfos) {
266 | stats.unfilteredCount += errorInfos.length;
267 |
268 | var errors = [];
269 | var errorToLine = {};
270 | for (var errorInfo in errorInfos) {
271 | for (var error in errorInfo.errors) {
272 | errors.add(error);
273 | errorToLine[error] = errorInfo.lineInfo;
274 | }
275 | }
276 |
277 | for (var error in errors) {
278 | formatError(errorToLine, error);
279 | }
280 | }
281 | }
282 |
283 | class HumanErrorFormatter extends ErrorFormatter {
284 | AnsiLogger ansi;
285 | bool displayCorrections;
286 |
287 | // This is a Set in order to de-dup CLI errors.
288 | Set batchedErrors = {};
289 |
290 | HumanErrorFormatter(super.out, super.stats,
291 | {super.severityProcessor,
292 | bool ansiColor = false,
293 | this.displayCorrections = false})
294 | : ansi = AnsiLogger(ansiColor);
295 |
296 | @override
297 | void flush() {
298 | // sort
299 | var sortedErrors = batchedErrors.toList()..sort();
300 |
301 | // print
302 | for (var error in sortedErrors) {
303 | if (error.isError) {
304 | stats.errorCount++;
305 | } else if (error.isWarning) {
306 | stats.warnCount++;
307 | } else if (error.isLint) {
308 | stats.lintCount++;
309 | } else if (error.isHint) {
310 | stats.hintCount++;
311 | }
312 |
313 | // warning • 'foo' is not a bar at lib/foo.dart:1:2 • foo_warning
314 | var issueColor = (error.isError || error.isWarning) ? ansi.red : '';
315 | out.write(' $issueColor${error.severity}${ansi.none} '
316 | '${ansi.bullet} ${ansi.bold}${error.message}${ansi.none} ');
317 | out.write('at ${error.sourcePath}');
318 | out.write(':${error.line}:${error.column} ');
319 | out.write('${ansi.bullet} ${error.errorCode}');
320 | out.writeln();
321 |
322 | if (displayCorrections) {
323 | out.writeln(
324 | '${' '.padLeft(error.severity.length + 2)}${error.correction}');
325 | }
326 | }
327 |
328 | // clear out batched errors
329 | batchedErrors.clear();
330 | }
331 |
332 | @override
333 | void formatError(
334 | Map errorToLine, AnalysisError error) {
335 | var source = error.source;
336 | var location = errorToLine[error]!.getLocation(error.offset);
337 |
338 | var severity = _severityProcessor(error);
339 |
340 | // Get display name; translate INFOs into LINTS and HINTS.
341 | var errorType = severity.displayName;
342 | if (severity == ErrorSeverity.INFO) {
343 | if (error.errorCode.type == ErrorType.HINT ||
344 | error.errorCode.type == ErrorType.LINT) {
345 | errorType = error.errorCode.type.displayName;
346 | }
347 | }
348 |
349 | // warning • 'foo' is not a bar at lib/foo.dart:1:2 • foo_warning
350 | var message = error.message;
351 | // Remove any terminating '.' from the end of the message.
352 | if (message.endsWith('.')) {
353 | message = message.substring(0, message.length - 1);
354 | }
355 | String sourcePath;
356 | if (source.uri.isScheme('dart')) {
357 | sourcePath = source.uri.toString();
358 | } else if (source.uri.isScheme('package')) {
359 | sourcePath = _relative(source.fullName);
360 | if (sourcePath == source.fullName) {
361 | // If we weren't able to shorten the path name, use the package: version.
362 | sourcePath = source.uri.toString();
363 | }
364 | } else {
365 | sourcePath = _relative(source.fullName);
366 | }
367 |
368 | batchedErrors.add(CLIError(
369 | severity: errorType,
370 | sourcePath: sourcePath,
371 | offset: error.offset,
372 | line: location.lineNumber,
373 | column: location.columnNumber,
374 | message: message,
375 | errorCode: error.errorCode.name.toLowerCase(),
376 | correction: error.correction,
377 | ));
378 | }
379 | }
380 |
--------------------------------------------------------------------------------
/lib/src/common.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import 'dart:io';
16 |
17 | import 'package:args/args.dart';
18 | import 'package:http/http.dart' as http;
19 | import 'package:yaml/yaml.dart';
20 |
21 | var _client = http.Client();
22 |
23 | Future getBody(String url) async => (await getResponse(url)).body;
24 |
25 | Future getResponse(String url) async =>
26 | _client.get(Uri.parse(url));
27 |
28 | /// Returns a [Future] that completes after the [event loop][] has run the given
29 | /// number of [times] (20 by default).
30 | ///
31 | /// [event loop]: https://webdev.dartlang.org/articles/performance/event-loop#darts-event-loop-and-queues
32 | ///
33 | /// Awaiting this approximates waiting until all asynchronous work (other than
34 | /// work that's waiting for external resources) completes.
35 | Future pumpEventQueue({int times = 20}) {
36 | if (times == 0) return Future.value();
37 | // Use [new Future] future to allow microtask events to finish. The [new
38 | // Future.value] constructor uses scheduleMicrotask itself and would therefore
39 | // not wait for microtask callbacks that are scheduled after invoking this
40 | // method.
41 | return Future(() => pumpEventQueue(times: times - 1));
42 | }
43 |
44 | double toDouble(Object value) {
45 | if (value is double) {
46 | return value;
47 | }
48 | if (value is String) {
49 | try {
50 | return double.parse(value);
51 | } on FormatException {
52 | rethrow;
53 | }
54 | }
55 | throw FormatException('$value cannot be parsed to a double');
56 | }
57 |
58 | int toInt(Object value) {
59 | if (value is int) {
60 | return value;
61 | }
62 | if (value is String) {
63 | try {
64 | return int.parse(value);
65 | } on FormatException {
66 | rethrow;
67 | }
68 | }
69 | throw FormatException('$value cannot be parsed to an int');
70 | }
71 |
72 | YamlMap _readYamlFromString(String optionsSource) {
73 | try {
74 | var doc = loadYamlNode(optionsSource);
75 | if (doc is YamlMap) {
76 | return doc;
77 | }
78 | return YamlMap();
79 | } on YamlException catch (e) {
80 | throw FormatException(e.message, e.span);
81 | } catch (e) {
82 | throw FormatException('Unable to parse YAML document.');
83 | }
84 | }
85 |
86 | class AnalysisOptionsFile {
87 | File file;
88 |
89 | String? _contents;
90 | YamlMap? _yaml;
91 |
92 | AnalysisOptionsFile(String path) : file = File(path);
93 |
94 | String get contents => _contents ??= file.readAsStringSync();
95 |
96 | /// Can throw a [FormatException] if yaml is malformed.
97 | YamlMap get yaml => _yaml ??= _readYamlFromString(contents);
98 | }
99 |
100 | class CommandLineOptions {
101 | /// Emit output in a verbose mode.
102 | bool verbose;
103 |
104 | /// Use ANSI color codes for output.
105 | bool color;
106 |
107 | /// Force installation of package dependencies.
108 | bool forceInstall;
109 |
110 | /// Skip package dependency install checks.
111 | bool skipInstall;
112 |
113 | /// Set a custom SDK location.
114 | String? sdk;
115 |
116 | CommandLineOptions({
117 | this.verbose = false,
118 | this.color = false,
119 | this.forceInstall = false,
120 | this.skipInstall = false,
121 | this.sdk,
122 | });
123 |
124 | CommandLineOptions.fromArgs(ArgResults args)
125 | : this(
126 | verbose: args['verbose'],
127 | color: args['color'],
128 | forceInstall: args['force-install'],
129 | skipInstall: args['skip-install'],
130 | sdk: args['sdk'],
131 | );
132 | }
133 |
134 | class PubspecFile {
135 | File file;
136 |
137 | String? _contents;
138 | YamlMap? _yaml;
139 |
140 | PubspecFile(String path) : file = File(path);
141 |
142 | String get contents => _contents ??= file.readAsStringSync();
143 |
144 | /// Can throw a [FormatException] if yaml is malformed.
145 | YamlMap get yaml => _yaml ??= _readYamlFromString(contents);
146 | }
147 |
--------------------------------------------------------------------------------
/lib/src/driver.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import 'dart:io' as io;
16 |
17 | import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
18 | import 'package:analyzer/dart/analysis/results.dart';
19 | import 'package:analyzer/dart/ast/ast.dart';
20 | import 'package:analyzer/file_system/file_system.dart';
21 | import 'package:analyzer/file_system/physical_file_system.dart';
22 | import 'package:analyzer/src/generated/engine.dart' // ignore: implementation_imports
23 | show
24 | AnalysisOptionsImpl;
25 | import 'package:analyzer/src/lint/linter.dart'; // ignore: implementation_imports
26 | import 'package:analyzer/src/lint/registry.dart'; // ignore: implementation_imports
27 | import 'package:args/args.dart';
28 | import 'package:cli_util/cli_logging.dart';
29 | import 'package:linter/src/rules.dart'; // ignore: implementation_imports
30 | import 'package:path/path.dart' as path;
31 |
32 | import 'common.dart';
33 | import 'install.dart';
34 | import 'visitors.dart';
35 |
36 | class Driver {
37 | CommandLineOptions options;
38 |
39 | /// Hook to contribute a custom AST visitor.
40 | AstVisitor? visitor;
41 |
42 | /// Hook to contribute custom options analysis.
43 | AnalysisOptionsVisitor? optionsVisitor;
44 |
45 | /// Hook to contribute custom pubspec analysis.
46 | PubspecVisitor? pubspecVisitor;
47 |
48 | /// List of paths to exclude from analysis.
49 | /// For example:
50 | /// ```
51 | /// driver.excludedPaths = ['example', 'test'];
52 | /// ```
53 | /// excludes package `example` and `test` directories.
54 | List excludedPaths = [];
55 |
56 | bool showErrors = true;
57 |
58 | bool resolveUnits = true;
59 |
60 | List sources;
61 |
62 | List? _lints;
63 |
64 | bool forceSkipInstall = false;
65 |
66 | bool silent = false;
67 |
68 | String? sdkPath;
69 |
70 | /// Handles printing. Can be overwritten by clients.
71 | Logger logger = Logger.standard();
72 |
73 | Driver(ArgResults argResults)
74 | : options = CommandLineOptions.fromArgs(argResults),
75 | sources = argResults.rest
76 | .map((p) => path.normalize(io.File(p).absolute.path))
77 | .toList() {
78 | sdkPath = options.sdk;
79 | }
80 |
81 | factory Driver.forArgs(List args) {
82 | var argParser = ArgParser()
83 | ..addFlag('verbose', abbr: 'v', help: 'verbose output.')
84 | ..addFlag('force-install', help: 'force package (re)installation.')
85 | ..addFlag('skip-install', help: 'skip package install checks.')
86 | ..addFlag('color', help: 'color output.')
87 | ..addOption('sdk', help: 'set a custom SDK path');
88 | var argResults = argParser.parse(args);
89 | return Driver(argResults);
90 | }
91 |
92 | bool get forcePackageInstall => options.forceInstall;
93 |
94 | List? get lints => _lints;
95 |
96 | /// Hook to contribute custom lint rules.
97 | set lints(List? lints) {
98 | if (lints != null) {
99 | // Ensure lints are registered
100 | for (var lint in lints) {
101 | Registry.ruleRegistry.register(lint);
102 | }
103 | }
104 | _lints = lints;
105 | }
106 |
107 | bool get skipPackageInstall => forceSkipInstall || options.skipInstall;
108 |
109 | Future analyze(
110 | {bool displayTiming = false, bool requirePackagesFile = true}) {
111 | var analysisFuture =
112 | _analyze(sources, requirePackagesFile: requirePackagesFile);
113 | if (!displayTiming) return analysisFuture;
114 |
115 | var stopwatch = Stopwatch()..start();
116 | return analysisFuture.then((value) {
117 | print(
118 | '(Elapsed time: ${Duration(milliseconds: stopwatch.elapsedMilliseconds)})');
119 | });
120 | }
121 |
122 | /// Hook to influence context post analysis.
123 | void postAnalyze(SurveyorContext context, DriverCommands callback) {
124 | if (visitor is PostAnalysisCallback) {
125 | (visitor as PostAnalysisCallback).postAnalysis(context, callback);
126 | }
127 | }
128 |
129 | /// Hook to influence context before analysis.
130 | void preAnalyze(SurveyorContext context, {bool? subDir}) {
131 | if (visitor is PreAnalysisCallback) {
132 | (visitor as PreAnalysisCallback).preAnalysis(context, subDir: subDir);
133 | }
134 | }
135 |
136 | Future _analyze(List sourceDirs,
137 | {required bool requirePackagesFile}) async {
138 | if (sourceDirs.isEmpty) {
139 | _print('Specify one or more files and directories.');
140 | return;
141 | }
142 | ResourceProvider resourceProvider = PhysicalResourceProvider.INSTANCE;
143 | await _analyzeFiles(resourceProvider, sourceDirs,
144 | requirePackagesFile: requirePackagesFile);
145 | _print('Finished.');
146 | }
147 |
148 | Future _analyzeFiles(
149 | ResourceProvider resourceProvider, List analysisRoots,
150 | {required bool requirePackagesFile}) async {
151 | if (skipPackageInstall) {
152 | _print('(Skipping dependency checks.)');
153 | }
154 |
155 | if (excludedPaths.isNotEmpty) {
156 | _print('(Excluding paths $excludedPaths from analysis.)');
157 | }
158 |
159 | // Analyze.
160 | _print('Analyzing...');
161 |
162 | var cmd = DriverCommands();
163 |
164 | registerLintRules();
165 |
166 | for (var root in analysisRoots) {
167 | if (cmd.continueAnalyzing) {
168 | var collection = AnalysisContextCollection(
169 | includedPaths: [root],
170 | excludedPaths: excludedPaths.map((p) => path.join(root, p)).toList(),
171 | resourceProvider: resourceProvider,
172 | sdkPath: sdkPath,
173 | );
174 |
175 | var lints = this.lints;
176 | for (var context in collection.contexts) {
177 | // Add custom lints.
178 | if (lints != null) {
179 | var options = context.analysisOptions as AnalysisOptionsImpl;
180 | options.lintRules = context.analysisOptions.lintRules.toList();
181 | for (var lint in lints) {
182 | options.lintRules.add(lint);
183 | }
184 | options.lint = true;
185 | }
186 | var dir = context.contextRoot.root.path;
187 | var package = Package(dir);
188 | // Ensure dependencies are installed.
189 | if (!skipPackageInstall) {
190 | await package.installDependencies(
191 | force: forcePackageInstall, silent: silent);
192 | }
193 |
194 | // Skip analysis if no .packages.
195 | if (requirePackagesFile && !package.packageConfigFile.existsSync()) {
196 | _print('No package config file in $dir (skipping analysis)');
197 | continue;
198 | }
199 |
200 | var surveyorContext = SurveyorContext(context);
201 |
202 | preAnalyze(surveyorContext, subDir: dir != root);
203 |
204 | for (var filePath in context.contextRoot.analyzedFiles()) {
205 | if (isDartFileName(filePath)) {
206 | try {
207 | var result = resolveUnits
208 | ? await context.currentSession.getResolvedUnit(filePath)
209 | as ResolvedUnitResult
210 | : context.currentSession.getParsedUnit(filePath)
211 | as ParsedUnitResult;
212 |
213 | var visitor = this.visitor;
214 | if (visitor != null) {
215 | if (visitor is ErrorReporter) {
216 | (visitor as ErrorReporter).reportError(result);
217 | }
218 | if (visitor is AstContext) {
219 | var astContext = visitor as AstContext;
220 | astContext.setLineInfo(result.lineInfo);
221 | astContext.setFilePath(filePath);
222 | }
223 | if (result is ParsedUnitResult) {
224 | result.unit.accept(visitor);
225 | } else if (result is ResolvedUnitResult) {
226 | result.unit.accept(visitor);
227 | }
228 | }
229 | } catch (e) {
230 | _print('Exception caught analyzing: $filePath');
231 | _print(e.toString());
232 | }
233 | }
234 |
235 | var astVisitor = visitor;
236 | var optionsVisitor = astVisitor is AnalysisOptionsVisitor
237 | ? astVisitor as AnalysisOptionsVisitor
238 | : this.optionsVisitor;
239 | if (optionsVisitor != null) {
240 | if (path.basename(filePath) == 'analysis_options.yaml') {
241 | optionsVisitor.visit(AnalysisOptionsFile(filePath));
242 | }
243 | }
244 |
245 | var pubspecVisitor = astVisitor is PubspecVisitor
246 | ? astVisitor as PubspecVisitor
247 | : this.pubspecVisitor;
248 | if (pubspecVisitor != null) {
249 | if (path.basename(filePath) == 'pubspec.yaml') {
250 | pubspecVisitor.visit(PubspecFile(filePath));
251 | }
252 | }
253 | }
254 |
255 | await pumpEventQueue(times: 512);
256 | postAnalyze(surveyorContext, cmd);
257 | }
258 | }
259 | }
260 |
261 | if (visitor is PostVisitCallback) {
262 | (visitor as PostVisitCallback).onVisitFinished();
263 | }
264 | }
265 |
266 | /// Pass the following [msg] to the [logger] instance iff [silent] is false.
267 | void _print(String msg) {
268 | if (!silent) {
269 | logger.stdout(msg);
270 | }
271 | }
272 |
273 | /// Returns `true` if this [fileName] is an analysis options file.
274 | static bool isAnalysisOptionsFileName(String fileName) =>
275 | fileName == 'analysis_options.yaml';
276 |
277 | /// Returns `true` if this [fileName] is a Dart file.
278 | static bool isDartFileName(String fileName) => fileName.endsWith('.dart');
279 | }
280 |
281 | class DriverCommands {
282 | bool continueAnalyzing = true;
283 | }
284 |
--------------------------------------------------------------------------------
/lib/src/install.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import 'dart:io';
16 |
17 | import 'package:path/path.dart' as pathutil;
18 | import 'package:yaml/yaml.dart' as yaml;
19 |
20 | class Package {
21 | static final _Installer _installer = _Installer();
22 |
23 | final Directory dir;
24 | Package(String path) : dir = Directory(path);
25 |
26 | Map get dependencies {
27 | var deps = pubspec['dependencies']?.value;
28 | if (deps is yaml.YamlMap) {
29 | return deps.nodes
30 | .map((k, v) => MapEntry(k.toString(), v));
31 | }
32 | return {};
33 | }
34 |
35 | File get packageConfigFile =>
36 | File(pathutil.join(dir.path, '.dart_tool', 'package_config.json'));
37 |
38 | Map get pubspec {
39 | var file = pubspecFile;
40 | if (file.existsSync()) {
41 | try {
42 | return (yaml.loadYaml(file.readAsStringSync()) as yaml.YamlMap).nodes;
43 | } on yaml.YamlException {
44 | // Warn?
45 | }
46 | }
47 | return {};
48 | }
49 |
50 | File get pubspecFile => File(pathutil.join(dir.path, 'pubspec.yaml'));
51 |
52 | Future installDependencies(
53 | {bool force = false, bool silent = false}) async {
54 | if (!force && _installer.hasDependenciesInstalled(this)) {
55 | return false;
56 | }
57 |
58 | await _installer.installDependencies(this, silent: silent);
59 | return true;
60 | }
61 | }
62 |
63 | class _Installer {
64 | bool hasDependenciesInstalled(Package package) =>
65 | package.dir.existsSync() && package.packageConfigFile.existsSync();
66 |
67 | Future installDependencies(Package package,
68 | {bool silent = false}) async {
69 | var sourcePath = package.dir.path;
70 | if (!package.dir.existsSync()) {
71 | _print(
72 | 'Unable to install dependencies: $sourcePath does not exist', silent);
73 | return null;
74 | }
75 | if (!package.pubspecFile.existsSync()) {
76 | return null;
77 | }
78 |
79 | if (package.dependencies.containsKey('flutter') == true) {
80 | _print(
81 | 'Running "flutter packages get" in ${pathutil.basename(sourcePath)}',
82 | silent);
83 | return Process.run('flutter', ['packages', 'get'],
84 | workingDirectory: sourcePath, runInShell: true);
85 | }
86 |
87 | _print('Running "pub get" in ${pathutil.basename(sourcePath)}', silent);
88 | return Process.run('dart', ['pub', 'get'],
89 | workingDirectory: sourcePath, runInShell: true);
90 | }
91 |
92 | /// Display the following [msg] to stdout iff [silent] is false.
93 | void _print(String msg, bool silent) {
94 | if (!silent) {
95 | print(msg);
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/lib/src/visitors.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import 'dart:io';
16 |
17 | import 'package:analyzer/dart/analysis/analysis_context.dart';
18 | import 'package:analyzer/dart/analysis/results.dart';
19 | import 'package:analyzer/source/line_info.dart';
20 |
21 | import 'common.dart';
22 | import 'driver.dart';
23 |
24 | /// A simple visitor for analysis options files.
25 | abstract class AnalysisOptionsVisitor {
26 | void visit(AnalysisOptionsFile file) {}
27 | }
28 |
29 | abstract class AstContext {
30 | void setFilePath(String filePath);
31 | void setLineInfo(LineInfo lineInfo);
32 | }
33 |
34 | /// Hook for custom error reporting.
35 | abstract class ErrorReporter {
36 | void reportError(AnalysisResultWithErrors result);
37 | }
38 |
39 | class OptionsVisitor extends AnalysisOptionsVisitor {
40 | @override
41 | void visit(AnalysisOptionsFile file) {
42 | //print('>> visiting: ${file.file}');
43 | }
44 | }
45 |
46 | /// A simple visitor for package roots.
47 | abstract class PackageRootVisitor {
48 | void visit(Directory root) {}
49 | }
50 |
51 | abstract class PostAnalysisCallback {
52 | void postAnalysis(SurveyorContext context, DriverCommands commandCallback);
53 | }
54 |
55 | abstract class PostVisitCallback {
56 | void onVisitFinished();
57 | }
58 |
59 | abstract class PreAnalysisCallback {
60 | void preAnalysis(SurveyorContext context,
61 | {bool? subDir, DriverCommands? commandCallback});
62 | }
63 |
64 | /// A simple visitor for pubspec files.
65 | abstract class PubspecVisitor {
66 | void visit(PubspecFile file) {}
67 | }
68 |
69 | class SurveyorContext {
70 | final AnalysisContext analysisContext;
71 | SurveyorContext(this.analysisContext);
72 | }
73 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: surveyor
2 | description: Tools for surveying Dart sources
3 | version: 1.0.0-dev.3.0
4 | repository: https://github.com/pq/surveyor
5 |
6 | environment:
7 | sdk: '>=2.17.0 <4.0.0'
8 |
9 | dependencies:
10 | analyzer: ^5.4.0
11 | args: ^2.0.0
12 | cli_util: ^0.4.0
13 | http: ^0.13.0
14 | linter: ^1.0.0
15 | path: ^1.8.0
16 | pub_semver: ^2.0.0
17 | yaml: ^3.1.0
18 |
19 | dev_dependencies:
20 | lints: ^3.0.0
21 | test: ^1.20.0
22 |
--------------------------------------------------------------------------------
/test/core_lib_use_surveyor_test.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2022 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import 'package:test/test.dart';
16 |
17 | import '../example/core_lib_use_surveyor.dart';
18 |
19 | main() async {
20 | var occurrences = await survey(['test_data/core_lib_use_surveyor']);
21 | var data = occurrences.data['core_lib_use_surveyor']!;
22 |
23 | void expectSymbols(String library, List symbols) {
24 | test(library, () {
25 | expect(data[library], unorderedEquals(symbols));
26 | });
27 | }
28 |
29 | expectSymbols('dart.convert', [
30 | 'jsonDecode' // top-level function
31 | ]);
32 |
33 | expectSymbols('dart.core', [
34 | 'parse',
35 | 'print',
36 | 'DateTime', // class, referenced via static field
37 | 'override', // annotation
38 | 'Object',
39 | 'String',
40 | ]);
41 |
42 | expectSymbols('dart.io', [
43 | 'FileMode', 'File', // classes
44 | 'exitCode', // top-level variable
45 | ]);
46 |
47 | // todo(pq): no mixins in core libs?
48 | }
49 |
--------------------------------------------------------------------------------
/test_data/core_lib_use_surveyor/lib/data.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2022 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | // ignore_for_file: deprecated_member_use, unused_local_variable
16 |
17 | import 'dart:convert';
18 | import 'dart:io';
19 |
20 | void main() {
21 | var x = FileMode.append;
22 | var y = int.parse('9');
23 | var f = File('')..writeAsStringSync(jsonDecode(''));
24 | print(f.length());
25 |
26 | var c = HttpClientResponseCompressionState.compressed;
27 |
28 | print(DateTime.april);
29 | print(exitCode);
30 | }
31 |
32 | class A extends Object {
33 | @override
34 | String toString() => '';
35 | }
36 |
--------------------------------------------------------------------------------
/test_data/core_lib_use_surveyor/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: core_lib_use_test_data
2 | description: Test data
3 | version: 0.1.0
4 |
5 | # Not intended for publishing.
6 | publish_to: none
7 |
8 | environment:
9 | sdk: '>=2.12.0-0 <3.0.0'
10 |
11 |
12 |
--------------------------------------------------------------------------------
/tool/de_dup.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import 'dart:io';
16 |
17 | /// Find and delete duplicate packages in a directory.
18 | void main(List args) async {
19 | var dir = args[0];
20 |
21 | var seen = {};
22 |
23 | var packages = Directory(dir).listSync().map((f) => f.path).toList()..sort();
24 | for (var package in packages) {
25 | // cache/flutter_util-0.0.1 => flutter_util
26 | var name = package.split('/').last.split('-').first;
27 | var previous = seen[name];
28 | if (previous != null) {
29 | print('deleting $previous, favoring $package');
30 | Directory(previous).deleteSync(recursive: true);
31 | }
32 | seen[name] = package;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tool/filter_dart2.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import 'dart:io';
16 |
17 | import 'package:pub_semver/pub_semver.dart';
18 | import 'package:surveyor/src/common.dart';
19 |
20 | void main(List args) async {
21 | var dir = args[0];
22 |
23 | var count = 0;
24 | var dart2 = VersionConstraint.parse('>=2.0.0');
25 |
26 | var packages = Directory(dir).listSync().toList();
27 | for (var package in packages) {
28 | try {
29 | var pubspec = PubspecFile('${package.path}/pubspec.yaml');
30 | var yaml = pubspec.yaml;
31 | var sdkVersion = yaml['environment']['sdk'];
32 | var constraint = VersionConstraint.parse(sdkVersion);
33 |
34 | if (!constraint.allowsAny(dart2)) {
35 | print('removing: $package ($sdkVersion)');
36 | await package.delete(recursive: true);
37 | ++count;
38 | }
39 | } catch (_) {
40 | // Ignore.
41 | }
42 | }
43 |
44 | print('Removed: $count packages.');
45 | }
46 |
--------------------------------------------------------------------------------
/tool/find_authors.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import 'dart:convert';
16 |
17 | import 'package:surveyor/src/common.dart';
18 |
19 | /// Find package authors.
20 | void main(List args) async {
21 | var packages = [
22 | '_discoveryapis_commons',
23 | 'alpha',
24 | 'angel_cli',
25 | 'angel_orm_generator',
26 | 'angular',
27 | 'angular_aria',
28 | 'angular_bloc',
29 | 'angular_dart_ui_bootstrap',
30 | 'ansi_color_palette',
31 | 'appengine',
32 | 'args',
33 | 'asset_pack',
34 | 'async',
35 | 'build_runner',
36 | 'buildbucket',
37 | 'chessboard',
38 | 'codable',
39 | 'code_builder',
40 | 'csp_fixer',
41 | 'cupid',
42 | 'dart2_constant',
43 | 'dart_browser_loader',
44 | 'dartlr',
45 | 'dartmon',
46 | 'dataset',
47 | 'deny',
48 | 'devtools',
49 | 'dilithium',
50 | 'discoveryapis_generator',
51 | 'disposable',
52 | 'expire_cache',
53 | 'fake_async',
54 | 'fancy_syntax',
55 | 'floor_generator',
56 | 'flutter_flux',
57 | 'flutter_wordpress',
58 | 'force_elements',
59 | 'front_end',
60 | 'google_adsense_v1_1_api',
61 | 'google_adsense_v1_api',
62 | 'google_compute_v1beta14_api',
63 | 'google_latitude_v1_api',
64 | 'google_maps',
65 | 'google_plus_widget',
66 | 'googleapis',
67 | 'googleapis_beta',
68 | 'gorgon',
69 | 'html_builder',
70 | 'html_components',
71 | 'http',
72 | 'ice_code_editor',
73 | 'ice_code_editor_experimental',
74 | 'intl',
75 | 'iris',
76 | 'js_wrapping',
77 | 'kernel',
78 | 'kourim',
79 | 'libpq93_bindings',
80 | 'mandrill_api',
81 | 'example',
82 | 'mobx',
83 | 'ngx_core',
84 | 'node_webkit',
85 | 'nuxeo_automation',
86 | 'over_react',
87 | 'play_phaser',
88 | 'play_pixi',
89 | 'plummbur_kruk',
90 | 'pool',
91 | 'pref_gen',
92 | 'pub_proxy_server',
93 | 'quiver',
94 | 'rails_ujs',
95 | 'rate_limit',
96 | 'reflutter_generator',
97 | 'remote_services',
98 | 'rosetta_generator',
99 | 'simple_auth_generator',
100 | 'socket_io_client',
101 | 'socket_io_common_client',
102 | 'solitaire',
103 | 'streamy',
104 | 'superlu',
105 | 'teaolive',
106 | 'test_api',
107 | 'three',
108 | 'uix',
109 | 'universal_html',
110 | 'universal_io',
111 | 'vint',
112 | 'web_ui',
113 | 'webapper',
114 | 'webdriver',
115 | 'webui_tasks',
116 | 'wui_builder',
117 | 'zx',
118 | ];
119 |
120 | for (var p in packages) {
121 | var json = jsonDecode(await getBody('https://pub.dev/api/packages/$p'));
122 | var details = hasDartAuthor(json)
123 | ? ' (Dart)'
124 | : hasFlutterAuthor(json)
125 | ? ' (Flutter)'
126 | : '';
127 | print('$p$details');
128 | }
129 | }
130 |
131 | bool hasDartAuthor(json) {
132 | var latest = json['latest'];
133 | var author = latest['pubspec']['author'];
134 | return author?.contains('Dart Team') == true;
135 | }
136 |
137 | bool hasFlutterAuthor(json) {
138 | var latest = json['latest'];
139 | var author = latest['pubspec']['author'];
140 | return author?.contains('Flutter Team') == true;
141 | }
142 |
--------------------------------------------------------------------------------
/tool/spelunk.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import 'dart:io';
16 |
17 | import 'package:analyzer/dart/analysis/features.dart';
18 | import 'package:analyzer/dart/ast/ast.dart';
19 | import 'package:analyzer/dart/ast/token.dart';
20 | import 'package:analyzer/dart/ast/visitor.dart';
21 | import 'package:analyzer/error/error.dart';
22 | import 'package:analyzer/error/listener.dart';
23 | import 'package:analyzer/source/line_info.dart';
24 | import 'package:analyzer/src/dart/scanner/reader.dart';
25 | import 'package:analyzer/src/dart/scanner/scanner.dart';
26 | import 'package:analyzer/src/generated/parser.dart' show Parser;
27 | import 'package:analyzer/src/string_source.dart' show StringSource;
28 | import 'package:pub_semver/pub_semver.dart';
29 |
30 | void main(List args) {
31 | if (args.length != 1) {
32 | throw Exception('Provide a path to a file to spelunk');
33 | }
34 |
35 | Spelunker(args[0]).spelunk();
36 | }
37 |
38 | class Spelunker {
39 | final String path;
40 | final IOSink sink;
41 |
42 | Spelunker(this.path, {IOSink? sink}) : sink = sink ?? stdout;
43 |
44 | void spelunk() {
45 | var contents = File(path).readAsStringSync();
46 |
47 | var errorListener = _ErrorListener();
48 |
49 | var reader = CharSequenceReader(contents);
50 | var stringSource = StringSource(contents, path);
51 | var featureSet = FeatureSet.fromEnableFlags2(
52 | sdkLanguageVersion: Version.parse('2.12.0'),
53 | flags: [],
54 | );
55 | var scanner = Scanner(stringSource, reader, errorListener)
56 | ..configureFeatures(
57 | featureSet: featureSet,
58 | featureSetForOverriding: featureSet,
59 | );
60 | var startToken = scanner.tokenize();
61 | errorListener.throwIfErrors();
62 |
63 | var lineInfo = LineInfo(scanner.lineStarts);
64 | var parser = Parser(stringSource, errorListener,
65 | featureSet: featureSet, lineInfo: lineInfo);
66 | var node = parser.parseCompilationUnit(startToken);
67 | errorListener.throwIfErrors();
68 |
69 | var visitor = _SourceVisitor(sink);
70 | node.accept(visitor);
71 | }
72 | }
73 |
74 | class _ErrorListener implements AnalysisErrorListener {
75 | final errors = [];
76 |
77 | @override
78 | void onError(AnalysisError error) {
79 | errors.add(error);
80 | }
81 |
82 | void throwIfErrors() {
83 | if (errors.isNotEmpty) {
84 | throw Exception(errors);
85 | }
86 | }
87 | }
88 |
89 | class _SourceVisitor extends GeneralizingAstVisitor {
90 | int indent = 0;
91 |
92 | final IOSink sink;
93 |
94 | _SourceVisitor(this.sink);
95 |
96 | String asString(AstNode node) =>
97 | '${typeInfo(node.runtimeType)} [${node.toString()}]';
98 |
99 | Iterable getPrecedingComments(Token token) sync* {
100 | Token? comment = token.precedingComments;
101 | while (comment != null) {
102 | yield comment;
103 | comment = comment.next;
104 | }
105 | }
106 |
107 | String getTrailingComment(AstNode node) {
108 | var successor = node.endToken.next;
109 | if (successor != null) {
110 | var precedingComments = successor.precedingComments;
111 | if (precedingComments != null) {
112 | return precedingComments.toString();
113 | }
114 | }
115 | return '';
116 | }
117 |
118 | String typeInfo(Type type) => type.toString();
119 |
120 | @override
121 | void visitNode(AstNode node) {
122 | write(node);
123 |
124 | ++indent;
125 | node.visitChildren(this);
126 | --indent;
127 | return;
128 | }
129 |
130 | void write(AstNode node) {
131 | //EOL comments
132 | var comments = getPrecedingComments(node.beginToken);
133 | for (var c in comments) {
134 | sink.writeln('${" " * indent}$c');
135 | }
136 | sink.writeln(
137 | '${" " * indent}${asString(node)} ${getTrailingComment(node)}');
138 | }
139 | }
140 |
--------------------------------------------------------------------------------