From 0ffe4409496e2c1c32f4625c4a5f8a97ebd98b7c Mon Sep 17 00:00:00 2001 From: evanweible-wf Date: Thu, 13 Aug 2015 14:24:28 -0500 Subject: [PATCH] #6 Add copy-license task. --- README.md | 19 ++++ lib/src/dart_dev_cli.dart | 2 + lib/src/tasks/config.dart | 2 + lib/src/tasks/copy_license/api.dart | 92 +++++++++++++++++++ lib/src/tasks/copy_license/cli.dart | 38 ++++++++ lib/src/tasks/copy_license/config.dart | 11 +++ lib/src/tasks/init/api.dart | 1 + .../has_licenses/NONSTANDARDLICENSE | 13 +++ .../has_licenses/example/index.html | 25 +++++ .../has_licenses/example/main.dart | 19 ++++ .../has_licenses/example/script.js | 17 ++++ .../has_licenses/example/style.css | 20 ++++ .../copy_license/has_licenses/pubspec.yaml | 5 + .../copy_license/has_licenses/tool/dev.dart | 11 +++ .../copy_license/no_license_file/pubspec.yaml | 5 + .../fixtures/copy_license/no_licenses/LICENSE | 13 +++ .../copy_license/no_licenses/lib/index.html | 9 ++ .../copy_license/no_licenses/lib/main.dart | 5 + .../copy_license/no_licenses/lib/script.js | 3 + .../copy_license/no_licenses/lib/style.css | 4 + .../copy_license/no_licenses/lib/text.txt | 0 .../copy_license/no_licenses/pubspec.yaml | 5 + test/integration/copy_license_test.dart | 85 +++++++++++++++++ 23 files changed, 404 insertions(+) create mode 100644 lib/src/tasks/copy_license/api.dart create mode 100644 lib/src/tasks/copy_license/cli.dart create mode 100644 lib/src/tasks/copy_license/config.dart create mode 100644 test/fixtures/copy_license/has_licenses/NONSTANDARDLICENSE create mode 100644 test/fixtures/copy_license/has_licenses/example/index.html create mode 100644 test/fixtures/copy_license/has_licenses/example/main.dart create mode 100644 test/fixtures/copy_license/has_licenses/example/script.js create mode 100644 test/fixtures/copy_license/has_licenses/example/style.css create mode 100644 test/fixtures/copy_license/has_licenses/pubspec.yaml create mode 100644 test/fixtures/copy_license/has_licenses/tool/dev.dart create mode 100644 test/fixtures/copy_license/no_license_file/pubspec.yaml create mode 100644 test/fixtures/copy_license/no_licenses/LICENSE create mode 100644 test/fixtures/copy_license/no_licenses/lib/index.html create mode 100644 test/fixtures/copy_license/no_licenses/lib/main.dart create mode 100644 test/fixtures/copy_license/no_licenses/lib/script.js create mode 100644 test/fixtures/copy_license/no_licenses/lib/style.css create mode 100644 test/fixtures/copy_license/no_licenses/lib/text.txt create mode 100644 test/fixtures/copy_license/no_licenses/pubspec.yaml create mode 100644 test/integration/copy_license_test.dart diff --git a/README.md b/README.md index fa0f021d..0ae7780e 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ All Dart (https://dartlang.org) projects eventually share a common set of develo - Consistent code formatting - Static analysis to detect issues - Examples for manual testing/exploration +- Applying a LICENSE file to all source files Together, the Dart SDK and a couple of packages from the Dart team supply the necessary tooling to support the above requirements. But, the usage is inconsistent, configuration is limited to command-line arguments, and you inevitably end @@ -59,6 +60,7 @@ static analysis - you just need to know how to use the `dart_dev` tool. - **Code Formatting:** runs the [`dartfmt` tool from the `dart_style` package](https://github.com/dart-lang/dart_style) over source code. - **Static Analysis:** runs the [`dartanalyzer`](https://www.dartlang.org/tools/analyzer/) over source code. - **Serving Examples:** uses [`pub serve`](https://www.dartlang.org/tools/pub/cmd/pub-serve.html) to serve the project examples. +- **Applying a License to Source Files:** copies a LICENSE file to all applicable files. ## Getting Started @@ -87,6 +89,9 @@ main(args) async { // Define the entry points for static analysis. config.analyze.entryPoints = ['lib/', 'test/', 'tool/']; + // Define the directories where the LICENSE should be applied. + config.copyLicense.directories = ['example/', 'lib/']; + // Configure the port on which examples should be served. config.examples.port = 9000; @@ -114,12 +119,14 @@ see the help usage. Try it out by running any of the following tasks: ``` # with the alias ddev analyze +ddev copy-license ddev examples ddev format ddev test # without the alias pub run dart_dev analyze +pub run dart_dev copy-license pub run dart_dev examples pub run dart_dev format pub run dart_dev test @@ -138,6 +145,7 @@ import 'package:dart_dev/dart_dev.dart'; main(args) async { // Available config objects: // config.analyze + // config.copyLicense // config.examples // config.format // config.init @@ -156,6 +164,15 @@ Name | Type | Default | Description `fatalWarnings` | `bool` | `true` | Treat non-type warnings as fatal. `hints` | `bool` | `true` | Show hint results. +### `copy-license` Config +All configuration options for the `copy-license` task are found on the `config.copyLicense` object. + +Name | Type | Default | Description +------------- | -------------- | ---------- | ----------- +`directories` | `List` | `['lib/']` | All source files in these directories will have the LICENSE header applied. +`licensePath` | `String` | `LICENSE` | Path to the source LICENSE file that will be copied to all source files. + + ### `examples` Config All configuration options for the `examples` task are found on the `config.examples` object. @@ -202,6 +219,7 @@ Usage: pub run dart_dev [task] [options] Supported tasks: analyze + copy-license examples format init @@ -209,6 +227,7 @@ Supported tasks: ``` - Static analysis: `ddev analyze` +- Applying license to source files: `ddev copy-license` - Serving examples: `ddev examples` - Dart formatter: `ddev format` - Initialization: `ddev init` diff --git a/lib/src/dart_dev_cli.dart b/lib/src/dart_dev_cli.dart index ddeb59b0..49575ad3 100644 --- a/lib/src/dart_dev_cli.dart +++ b/lib/src/dart_dev_cli.dart @@ -16,6 +16,7 @@ import 'package:dart_dev/src/tasks/cli.dart'; import 'package:dart_dev/src/tasks/config.dart'; import 'package:dart_dev/src/tasks/analyze/cli.dart'; +import 'package:dart_dev/src/tasks/copy_license/cli.dart'; import 'package:dart_dev/src/tasks/examples/cli.dart'; import 'package:dart_dev/src/tasks/format/cli.dart'; import 'package:dart_dev/src/tasks/init/cli.dart'; @@ -38,6 +39,7 @@ String _topLevelUsage = _parser.usage; dev(List args) async { registerTask(new AnalyzeCli(), config.analyze); + registerTask(new CopyLicenseCli(), config.copyLicense); registerTask(new ExamplesCli(), config.examples); registerTask(new FormatCli(), config.format); registerTask(new InitCli(), config.init); diff --git a/lib/src/tasks/config.dart b/lib/src/tasks/config.dart index 31984fb9..75daf1c9 100644 --- a/lib/src/tasks/config.dart +++ b/lib/src/tasks/config.dart @@ -1,6 +1,7 @@ library dart_dev.src.tasks.config; import 'package:dart_dev/src/tasks/analyze/config.dart'; +import 'package:dart_dev/src/tasks/copy_license/config.dart'; import 'package:dart_dev/src/tasks/examples/config.dart'; import 'package:dart_dev/src/tasks/format/config.dart'; import 'package:dart_dev/src/tasks/init/config.dart'; @@ -10,6 +11,7 @@ Config config = new Config(); class Config { AnalyzeConfig analyze = new AnalyzeConfig(); + CopyLicenseConfig copyLicense = new CopyLicenseConfig(); ExamplesConfig examples = new ExamplesConfig(); FormatConfig format = new FormatConfig(); InitConfig init = new InitConfig(); diff --git a/lib/src/tasks/copy_license/api.dart b/lib/src/tasks/copy_license/api.dart new file mode 100644 index 00000000..851df9f9 --- /dev/null +++ b/lib/src/tasks/copy_license/api.dart @@ -0,0 +1,92 @@ +library dart_dev.src.tasks.copy_license.api; + +import 'dart:async'; +import 'dart:io'; + +import 'package:dart_dev/src/tasks/copy_license/config.dart'; +import 'package:dart_dev/src/tasks/task.dart'; + +CopyLicenseTask copyLicense( + {List directories: defaultDirectories, + String licensePath: defaultLicensePath}) { + CopyLicenseTask task = new CopyLicenseTask(); + + File license = new File(licensePath); + if (!license.existsSync()) throw new Exception( + 'License file "$licensePath" does not exist.'); + + String licenseContents = license.readAsStringSync(); + directories.forEach((path) { + Directory dir = new Directory(path); + if (!dir.existsSync()) return; + Iterable files = + dir.listSync(recursive: true).where((e) => e is File); + files.forEach((file) { + // Skip files in packages/ directory + if (file.path.contains('/packages/')) return; + + if (applyLicense(file, licenseContents)) { + task.affectedFiles.add(file.path); + } + }); + }); + + task.successful = true; + return task; +} + +bool applyLicense(File file, String license) { + if (hasLicense(file, license)) return false; + + String fileLicense; + try { + fileLicense = licenseForFileType(file, license); + } catch (e) { + return false; + } + + String fileContents = file.readAsStringSync(); + file.writeAsStringSync('$fileLicense\n$fileContents'); + return true; +} + +bool hasLicense(File file, String license) { + String licenseHeader = license.split('\n').first; + String fileContents = file.readAsStringSync(); + Iterable lines = fileContents.split('\n'); + if (lines.isEmpty) return false; + if (lines.first.contains(licenseHeader)) return true; + if (lines.length <= 1) return false; + if (lines.elementAt(1).contains(licenseHeader)) return true; + return false; +} + +String licenseForFileType(File file, String license) { + String opening = ''; + String closing = ''; + String linePrefix = ''; + + if (file.path.endsWith('.css')) { + opening = '/**\n'; + linePrefix = ' * '; + closing = '\n */'; + } else if (file.path.endsWith('.dart')) { + linePrefix = '// '; + } else if (file.path.endsWith('.html')) { + opening = ''; + } else if (file.path.endsWith('.js')) { + linePrefix = '// '; + } else { + throw new ArgumentError('Unsupported file type: ${file.path}'); + } + + String l = license.split('\n').map((l) => '$linePrefix$l').join('\n'); + return '$opening$l$closing\n'; +} + +class CopyLicenseTask extends Task { + List affectedFiles = []; + final Future done = new Future.value(); + CopyLicenseTask(); +} diff --git a/lib/src/tasks/copy_license/cli.dart b/lib/src/tasks/copy_license/cli.dart new file mode 100644 index 00000000..8e91c528 --- /dev/null +++ b/lib/src/tasks/copy_license/cli.dart @@ -0,0 +1,38 @@ +library dart_dev.src.tasks.copy_license.cli; + +import 'dart:async'; +import 'dart:io'; + +import 'package:args/args.dart'; + +import 'package:dart_dev/src/tasks/copy_license/api.dart'; +import 'package:dart_dev/src/tasks/cli.dart'; +import 'package:dart_dev/src/tasks/config.dart'; + +class CopyLicenseCli extends TaskCli { + final ArgParser argParser = new ArgParser(); + + final String command = 'copy-license'; + + Future run(ArgResults parsedArgs) async { + List directories = config.copyLicense.directories; + String licensePath = config.copyLicense.licensePath; + + if (!(new File(licensePath)).existsSync()) return new CliResult.fail( + 'License file "$licensePath" does not exist.'); + + CopyLicenseTask task = + copyLicense(directories: directories, licensePath: licensePath); + await task.done; + if (task.successful) { + int numFiles = task.affectedFiles.length; + String fileSummary = '\n ${task.affectedFiles.join('\n ')}'; + String result = numFiles == 0 + ? 'License already exists on all files.' + : 'License successfully applied to $numFiles files:$fileSummary'; + return new CliResult.success(result); + } else { + return new CliResult.fail('License application failed.'); + } + } +} diff --git a/lib/src/tasks/copy_license/config.dart b/lib/src/tasks/copy_license/config.dart new file mode 100644 index 00000000..a0e20090 --- /dev/null +++ b/lib/src/tasks/copy_license/config.dart @@ -0,0 +1,11 @@ +library dart_dev.src.tasks.copy_license.config; + +import 'package:dart_dev/src/tasks/config.dart'; + +const List defaultDirectories = const ['lib/']; +const String defaultLicensePath = 'LICENSE'; + +class CopyLicenseConfig extends TaskConfig { + List directories = defaultDirectories; + String licensePath = defaultLicensePath; +} diff --git a/lib/src/tasks/init/api.dart b/lib/src/tasks/init/api.dart index 2a5e74d5..337f5d9e 100644 --- a/lib/src/tasks/init/api.dart +++ b/lib/src/tasks/init/api.dart @@ -16,6 +16,7 @@ main(List args) async { // Available task configurations: // config.analyze + // config.copyLicense // config.examples // config.format // config.test diff --git a/test/fixtures/copy_license/has_licenses/NONSTANDARDLICENSE b/test/fixtures/copy_license/has_licenses/NONSTANDARDLICENSE new file mode 100644 index 00000000..3562dd3d --- /dev/null +++ b/test/fixtures/copy_license/has_licenses/NONSTANDARDLICENSE @@ -0,0 +1,13 @@ +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/test/fixtures/copy_license/has_licenses/example/index.html b/test/fixtures/copy_license/has_licenses/example/index.html new file mode 100644 index 00000000..6dd35eba --- /dev/null +++ b/test/fixtures/copy_license/has_licenses/example/index.html @@ -0,0 +1,25 @@ + + + + + + Hello + + +This should be copyrighted. + + \ No newline at end of file diff --git a/test/fixtures/copy_license/has_licenses/example/main.dart b/test/fixtures/copy_license/has_licenses/example/main.dart new file mode 100644 index 00000000..49e67930 --- /dev/null +++ b/test/fixtures/copy_license/has_licenses/example/main.dart @@ -0,0 +1,19 @@ +// Copyright [yyyy] [name of copyright owner] +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +library copy_license_no_licenses; + +class TopSecret { + String info = '[redacted]'; +} \ No newline at end of file diff --git a/test/fixtures/copy_license/has_licenses/example/script.js b/test/fixtures/copy_license/has_licenses/example/script.js new file mode 100644 index 00000000..35384f91 --- /dev/null +++ b/test/fixtures/copy_license/has_licenses/example/script.js @@ -0,0 +1,17 @@ +// Copyright [yyyy] [name of copyright owner] +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +(function () { + document.body = null; +})(); \ No newline at end of file diff --git a/test/fixtures/copy_license/has_licenses/example/style.css b/test/fixtures/copy_license/has_licenses/example/style.css new file mode 100644 index 00000000..97f02764 --- /dev/null +++ b/test/fixtures/copy_license/has_licenses/example/style.css @@ -0,0 +1,20 @@ +/** + * Copyright [yyyy] [name of copyright owner] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +* { + background-color: #000; + color: #000; +} \ No newline at end of file diff --git a/test/fixtures/copy_license/has_licenses/pubspec.yaml b/test/fixtures/copy_license/has_licenses/pubspec.yaml new file mode 100644 index 00000000..d801bd9e --- /dev/null +++ b/test/fixtures/copy_license/has_licenses/pubspec.yaml @@ -0,0 +1,5 @@ +name: copy_license_has_licenses +version: 0.0.0 +dev_dependencies: + dart_dev: + path: ../../../.. \ No newline at end of file diff --git a/test/fixtures/copy_license/has_licenses/tool/dev.dart b/test/fixtures/copy_license/has_licenses/tool/dev.dart new file mode 100644 index 00000000..8fc12b1e --- /dev/null +++ b/test/fixtures/copy_license/has_licenses/tool/dev.dart @@ -0,0 +1,11 @@ +library tool.dev; + +import 'package:dart_dev/dart_dev.dart' show dev, config; + +main(List args) async { + config.copyLicense + ..directories = ['example/'] + ..licensePath = 'NONSTANDARDLICENSE'; + + await dev(args); +} diff --git a/test/fixtures/copy_license/no_license_file/pubspec.yaml b/test/fixtures/copy_license/no_license_file/pubspec.yaml new file mode 100644 index 00000000..820d504c --- /dev/null +++ b/test/fixtures/copy_license/no_license_file/pubspec.yaml @@ -0,0 +1,5 @@ +name: copy_license_no_license_file +version: 0.0.0 +dev_dependencies: + dart_dev: + path: ../../../.. \ No newline at end of file diff --git a/test/fixtures/copy_license/no_licenses/LICENSE b/test/fixtures/copy_license/no_licenses/LICENSE new file mode 100644 index 00000000..3562dd3d --- /dev/null +++ b/test/fixtures/copy_license/no_licenses/LICENSE @@ -0,0 +1,13 @@ +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/test/fixtures/copy_license/no_licenses/lib/index.html b/test/fixtures/copy_license/no_licenses/lib/index.html new file mode 100644 index 00000000..1b605f9b --- /dev/null +++ b/test/fixtures/copy_license/no_licenses/lib/index.html @@ -0,0 +1,9 @@ + + + + Hello + + +This should be copyrighted. + + \ No newline at end of file diff --git a/test/fixtures/copy_license/no_licenses/lib/main.dart b/test/fixtures/copy_license/no_licenses/lib/main.dart new file mode 100644 index 00000000..4033013a --- /dev/null +++ b/test/fixtures/copy_license/no_licenses/lib/main.dart @@ -0,0 +1,5 @@ +library copy_license_no_licenses; + +class TopSecret { + String info = '[redacted]'; +} \ No newline at end of file diff --git a/test/fixtures/copy_license/no_licenses/lib/script.js b/test/fixtures/copy_license/no_licenses/lib/script.js new file mode 100644 index 00000000..83ecab94 --- /dev/null +++ b/test/fixtures/copy_license/no_licenses/lib/script.js @@ -0,0 +1,3 @@ +(function () { + document.body = null; +})(); \ No newline at end of file diff --git a/test/fixtures/copy_license/no_licenses/lib/style.css b/test/fixtures/copy_license/no_licenses/lib/style.css new file mode 100644 index 00000000..d3d1fcf3 --- /dev/null +++ b/test/fixtures/copy_license/no_licenses/lib/style.css @@ -0,0 +1,4 @@ +* { + background-color: #000; + color: #000; +} \ No newline at end of file diff --git a/test/fixtures/copy_license/no_licenses/lib/text.txt b/test/fixtures/copy_license/no_licenses/lib/text.txt new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/copy_license/no_licenses/pubspec.yaml b/test/fixtures/copy_license/no_licenses/pubspec.yaml new file mode 100644 index 00000000..83c5c5b1 --- /dev/null +++ b/test/fixtures/copy_license/no_licenses/pubspec.yaml @@ -0,0 +1,5 @@ +name: copy_license_no_licenses +version: 0.0.0 +dev_dependencies: + dart_dev: + path: ../../../.. \ No newline at end of file diff --git a/test/integration/copy_license_test.dart b/test/integration/copy_license_test.dart new file mode 100644 index 00000000..950e1442 --- /dev/null +++ b/test/integration/copy_license_test.dart @@ -0,0 +1,85 @@ +library dart_dev.test.integration.copy_license_test; + +import 'dart:async'; +import 'dart:io'; + +import 'package:dart_dev/util.dart' show TaskProcess; +import 'package:test/test.dart'; + +const String projectWithLicenses = 'test/fixtures/copy_license/has_licenses'; +const String projectWithoutLicenseFile = + 'test/fixtures/copy_license/no_license_file'; +const String projectWithoutLicenses = 'test/fixtures/copy_license/no_licenses'; + +Future createTemporaryProject(String source) async { + String tempProject = '${source}_temp'; + Directory temp = new Directory(tempProject); + temp.createSync(); + await Process.run('cp', ['-R', '$source/', tempProject]); + return tempProject; +} + +Future> copyLicense(String projectPath) async { + await Process.run('pub', ['get'], workingDirectory: projectPath); + + TaskProcess process = new TaskProcess( + 'pub', ['run', 'dart_dev', 'copy-license', '--no-color'], + workingDirectory: projectPath); + + String licenseAppliedPattern = 'License successfully applied'; + String noLicenseFilePattern = 'does not exist'; + bool didApplyLicense = false; + bool noLicenseFile = false; + List files = []; + + await for (var line in process.stdout) { + if (didApplyLicense) { + files.add(line.trim()); + } + if (line.contains(licenseAppliedPattern)) { + didApplyLicense = true; + } + } + + await for (var line in process.stderr) { + if (line.contains(noLicenseFilePattern)) { + noLicenseFile = true; + } + } + + await process.done; + if (noLicenseFile) throw new NoLicenseFileException(); + return files; +} + +void deleteTemporaryProject(String path) { + Directory temp = new Directory(path); + temp.deleteSync(recursive: true); +} + +class NoLicenseFileException implements Exception {} + +void main() { + group('Copy License Task', () { + test('should warn if the license file does not exist', () async { + expect(copyLicense(projectWithoutLicenseFile), + throwsA(new isInstanceOf())); + }); + + test('should not apply license to files that already have it', () async { + expect(await copyLicense(projectWithLicenses), isEmpty); + }); + + test('should apply license to all files that need it', () async { + String projectPath = await createTemporaryProject(projectWithoutLicenses); + List expectedFiles = [ + 'lib/index.html', + 'lib/main.dart', + 'lib/script.js', + 'lib/style.css' + ]; + expect(await copyLicense(projectPath), unorderedEquals(expectedFiles)); + deleteTemporaryProject(projectPath); + }); + }); +}