Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/amplify_lints.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
- stable
paths:
- '.github/workflows/amplify_lints.yaml'
- '.github/workflows/dart_native.yaml'
- '.github/workflows/dart_vm.yaml'
- 'packages/amplify_lints/**/*.dart'
- 'packages/amplify_lints/**/*.yaml'
Expand All @@ -15,6 +16,7 @@ on:
pull_request:
paths:
- '.github/workflows/amplify_lints.yaml'
- '.github/workflows/dart_native.yaml'
- '.github/workflows/dart_vm.yaml'
- 'packages/amplify_lints/**/*.dart'
- 'packages/amplify_lints/**/*.yaml'
Expand Down Expand Up @@ -45,3 +47,10 @@ jobs:
with:
package-name: amplify_lints
working-directory: packages/amplify_lints
native_test:
needs: test
uses: ./.github/workflows/dart_native.yaml
secrets: inherit
with:
package-name: amplify_lints
working-directory: packages/amplify_lints
23 changes: 23 additions & 0 deletions packages/amplify_lints/lib/amplify_lints.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

/// Custom lint rules for Amplify Flutter packages.
///
/// This package provides custom lint rules using `custom_lint` that are
/// automatically discovered by the Dart Analysis Server when `custom_lint`
/// is enabled in `analysis_options.yaml` and this package is a dependency.
library amplify_lints;

import 'package:custom_lint_builder/custom_lint_builder.dart';

import 'src/lints/missing_license_header.dart';

/// Creates the plugin instance for custom_lint to discover.
PluginBase createPlugin() => _AmplifyLintsPlugin();

class _AmplifyLintsPlugin extends PluginBase {
@override
List<LintRule> getLintRules(CustomLintConfigs configs) => [
MissingLicenseHeader(),
];
}
5 changes: 4 additions & 1 deletion packages/amplify_lints/lib/app.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
include: package:flutter_lints/flutter.yaml

plugins:
amplify_lints: any

analyzer:
language:
strict-casts: true
Expand All @@ -14,7 +17,7 @@ linter:
- avoid_field_initializers_in_const_classes # To prefer using getters over fields.
- cancel_subscriptions # To avoid memory leaks and to prevent code from firing after a subscription is no longer being used.
- close_sinks # To avoid memory leaks.
- directives_ordering # To maintain visual separation of a files imports.
- directives_ordering # To maintain visual separation of a file's imports.
- eol_at_end_of_file # To provide consistency across our repos/languages.
- flutter_style_todos # To ensure traceability of TODOs.
- invalid_case_patterns # To prevent invalid case statements.
Expand Down
5 changes: 4 additions & 1 deletion packages/amplify_lints/lib/library.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
include: package:lints/recommended.yaml

plugins:
amplify_lints: any

analyzer:
language:
strict-casts: true
Expand Down Expand Up @@ -31,7 +34,7 @@ linter:
- conditional_uri_does_not_exist # To prevent accidentally referencing a nonexistent file.
- depend_on_referenced_packages # To prevent issues publishing.
- deprecated_consistency # To encourage correct usage of deprecation and provide a better DX.
- directives_ordering # To maintain visual separation of a files imports.
- directives_ordering # To maintain visual separation of a file's imports.
- eol_at_end_of_file # To provide consistency across our repos/languages.
- flutter_style_todos # To ensure traceability of TODOs.
- invalid_case_patterns # To prevent invalid case statements.
Expand Down
15 changes: 15 additions & 0 deletions packages/amplify_lints/lib/main.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

/// Entry point for the amplify_lints analyzer plugin.
///
/// This file is automatically discovered by the Dart Analysis Server
/// when amplify_lints is enabled as a plugin in `analysis_options.yaml`.
library;

import 'package:amplify_lints/src/plugin.dart';

export 'src/plugin.dart';

/// The top-level plugin instance required by the Dart Analysis Server.
final plugin = AmplifyLintsPlugin();
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import 'package:amplify_lints/src/lints/missing_license_header.dart';
import 'package:analysis_server_plugin/edit/dart/correction_producer.dart';
import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart';
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
import 'package:analyzer_plugin/utilities/fixes/fixes.dart';

/// A quick fix that adds the Amazon/Apache-2.0 license header to the top
/// of a Dart file.
class AddLicenseHeaderFix extends ResolvedCorrectionProducer {
/// Creates a new [AddLicenseHeaderFix].
AddLicenseHeaderFix({required super.context});

static const _fixKind = FixKind(
'amplify_lints.fix.addLicenseHeader',
DartFixKindPriority.standard,
'Add license header',
);

@override
CorrectionApplicability get applicability =>
CorrectionApplicability.acrossSingleFile;

@override
FixKind get fixKind => _fixKind;

@override
Future<void> compute(ChangeBuilder builder) async {
await builder.addDartFileEdit(file, (builder) {
builder.addSimpleInsertion(0, '${licenseHeader.join('\n')}\n\n');
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import 'package:analyzer/analysis_rule/analysis_rule.dart';
import 'package:analyzer/analysis_rule/rule_context.dart';
import 'package:analyzer/analysis_rule/rule_visitor_registry.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/error/error.dart';

/// The expected license header lines.
const licenseHeader = [
'// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.',
'// SPDX-License-Identifier: Apache-2.0',
];

/// A custom lint rule that checks for the presence of the standard
/// Amazon/Apache-2.0 license header at the top of every Dart file.
///
/// **Good:**
/// ```dart
/// // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
/// // SPDX-License-Identifier: Apache-2.0
///
/// library my_library;
/// ```
///
/// **Bad:**
/// ```dart
/// library my_library;
/// ```
class MissingLicenseHeader extends AnalysisRule {
/// Creates a new [MissingLicenseHeader] lint rule.
MissingLicenseHeader()
: super(
name: 'missing_license_header',
description:
'Dart files must start with the Amazon copyright and '
'Apache-2.0 license header.',
);

/// The lint code reported by this rule.
static const LintCode code = LintCode(
'missing_license_header',
'Dart files must start with the Amazon copyright and Apache-2.0 '
'license header.',
correctionMessage: 'Add the license header to the top of this file.',
);

@override
LintCode get diagnosticCode => code;

@override
void registerNodeProcessors(
RuleVisitorRegistry registry,
RuleContext context,
) {
final visitor = _Visitor(this, context);
registry.addCompilationUnit(this, visitor);
}
}

class _Visitor extends SimpleAstVisitor<void> {
_Visitor(this.rule, this.context);

final AnalysisRule rule;
final RuleContext context;

@override
void visitCompilationUnit(CompilationUnit node) {
final unit = context.currentUnit;
if (unit == null) return;

// Use the content provided by the analysis context (no need to read
// the file from disk).
if (_hasLicenseHeader(unit.content)) return;

// Report the lint on the first token in the file.
rule.reportAtToken(node.beginToken);
}
}

/// Returns `true` if [content] starts with the expected license header.
bool _hasLicenseHeader(String content) {
final lines = content.split('\n');
if (lines.length < licenseHeader.length) return false;

for (var i = 0; i < licenseHeader.length; i++) {
if (lines[i].trimRight() != licenseHeader[i]) return false;
}
return true;
}
28 changes: 28 additions & 0 deletions packages/amplify_lints/lib/src/plugin.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import 'package:amplify_lints/src/fixes/add_license_header_fix.dart';
import 'package:amplify_lints/src/lints/missing_license_header.dart';
import 'package:analysis_server_plugin/plugin.dart';
import 'package:analysis_server_plugin/registry.dart';

/// The amplify_lints analyzer plugin.
///
/// Registers custom lint rules and quick fixes for Amplify Flutter packages.
class AmplifyLintsPlugin extends Plugin {
@override
String get name => 'amplify_lints';

@override
void register(PluginRegistry registry) {
// Register lint rules (disabled by default, must be enabled in
// analysis_options.yaml).
registry.registerWarningRule(MissingLicenseHeader());

// Register quick fixes associated with lint rules.
registry.registerFixForRule(
MissingLicenseHeader.code,
AddLicenseHeaderFix.new,
);
}
}
8 changes: 8 additions & 0 deletions packages/amplify_lints/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,13 @@ environment:
sdk: ^3.9.0

dependencies:
analysis_server_plugin: any
analyzer: any
analyzer_plugin: any
flutter_lints: ^6.0.0
lints: ^6.0.0

dev_dependencies:
analyzer_testing: ^0.1.0
test: ^1.24.0
test_reflective_loader: ^0.2.0
109 changes: 109 additions & 0 deletions packages/amplify_lints/test/lints/missing_license_header_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import 'dart:io';

import 'package:test/test.dart';

/// The expected license header lines (mirrored from the lint rule).
const _licenseHeader = [
'// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.',
'// SPDX-License-Identifier: Apache-2.0',
];

/// Returns `true` if [content] starts with the expected license header.
///
/// This mirrors the private `_hasLicenseHeader` function from the lint rule
/// for unit testing purposes.
bool _hasLicenseHeader(String content) {
final lines = content.split('\n');
if (lines.length < _licenseHeader.length) return false;

for (var i = 0; i < _licenseHeader.length; i++) {
if (lines[i].trimRight() != _licenseHeader[i]) return false;
}
return true;
}

void main() {
late Directory tmpDir;

setUp(() {
tmpDir = Directory.systemTemp.createTempSync('license_header_test_');
});

tearDown(() {
tmpDir.deleteSync(recursive: true);
});

group('MissingLicenseHeader logic', () {
test('detects missing license header', () {
const content = 'void main() {}\n';
expect(_hasLicenseHeader(content), isFalse);
});

test('detects present license header', () {
const content =
'// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n'
'// SPDX-License-Identifier: Apache-2.0\n'
'\n'
'void main() {}\n';
expect(_hasLicenseHeader(content), isTrue);
});

test('detects missing SPDX line (only copyright present)', () {
const content =
'// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n'
'\n'
'void main() {}\n';
expect(_hasLicenseHeader(content), isFalse);
});

test('detects missing copyright line (only SPDX present)', () {
const content =
'// SPDX-License-Identifier: Apache-2.0\n'
'\n'
'void main() {}\n';
expect(_hasLicenseHeader(content), isFalse);
});

test('detects wrong order of header lines', () {
const content =
'// SPDX-License-Identifier: Apache-2.0\n'
'// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n'
'\n'
'void main() {}\n';
expect(_hasLicenseHeader(content), isFalse);
});

test('allows content after the header', () {
const content =
'// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n'
'// SPDX-License-Identifier: Apache-2.0\n'
'\n'
"import 'dart:core';\n"
'\n'
'class Foo {\n'
' void bar() {}\n'
'}\n';
expect(_hasLicenseHeader(content), isTrue);
});

test('allows header with trailing whitespace', () {
const content =
'// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. \n'
'// SPDX-License-Identifier: Apache-2.0 \n'
'\n'
'void main() {}\n';
expect(_hasLicenseHeader(content), isTrue);
});

test('rejects empty file', () {
expect(_hasLicenseHeader(''), isFalse);
});

test('rejects single line file', () {
expect(_hasLicenseHeader('void main() {}'), isFalse);
});
});
}
Loading