├── .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 | [![Build Status](https://github.com/pq/surveyor/actions/workflows/dart.yml/badge.svg)](https://github.com/pq/surveyor/actions) 5 | [![Pub](https://img.shields.io/pub/v/surveyor.svg)](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 | --------------------------------------------------------------------------------