Skip to content

Commit

Permalink
Enable failure on cycles. (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
polina-c authored Mar 18, 2024
1 parent 84424eb commit 33b200e
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 18 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ jobs:

- name: Run tests
run: dart test

- name: Check for cycles
run: dart run layerlens --fail-on-cycles
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 1.0.16

* Support argument `--fail-on-cycles`.

# 1.0.15

* Improve formatting.
Expand Down
37 changes: 26 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
# LayerLens

Generate dependency diagram in every folder of your Dart or Flutter
source code as [Mermaid `flowchart`](https://mermaid.js.org/syntax/flowchart.html) documents.
package as [Mermaid `flowchart`](https://mermaid.js.org/syntax/flowchart.html) documents.

<img width="536" alt="Screenshot 2023-01-14 at 9 45 33 PM" src="https://user-images.githubusercontent.com/12115586/212524921-5221785f-692d-4464-a230-0f620434e2c5.png">

## Disclaimer

This project is not an official Google project. It is not supported by
Google and Google specifically disclaims all warranties as to its quality,
merchantability, or fitness for a particular purpose.
NOTE: LayerLens shows inside-package dependencies. For cross-package dependencies use `flutter pub deps`.

## Configure layerlens

### Configure command globally

1. Run `dart pub global activate layerlens`

2. Verify you can run `layerlens`. If you get `command not found`, make sure
your path [contains pub cache](https://dart.dev/tools/pub/cmd/pub-global#running-a-script-from-your-path).

3. To see the diagrams in your IDE:
### Configure command for a package

1. Add `layerlens: <version>` to the section `dev_dependencies` in the package's pubspec.yaml.

2. Run `dart pub get` or `flutter pub get` for the package.

### Configure IDE

To see the diagrams in your IDE:

- **VSCode**: install `Markdown Preview Mermaid Support` extension

Expand All @@ -27,7 +34,11 @@ your path [contains pub cache](https://dart.dev/tools/pub/cmd/pub-global#running

## Generate diagrams

1. Run `layerlens <your package root>`
1. Run command:

- With global configuration: `layerlens --path <your package root>`

- With package configuration: `dart run layerlens` in the package root

2. Find the generated file DEPENDENCIES.md in each source folder, where
libraries or folders depend on each other.
Expand All @@ -45,14 +56,12 @@ to `.github/workflows`.
You may want to avoid circular references, because without circles:
1. Code is easier to maintain
2. Chance of memory leaks is smaller
3. Treeshaking (i.e. not includine non-used code into build) is more efficient
3. Treeshaking (i.e. not including non-used code into build) is more efficient
4. Incremental build is faster

LayerLens marks inverted dependencies (dependencies that create circles) with '!'.

If, in addition, you want presubmit alerting for circular references,
upvote [the issue](https://github.com/polina-c/layerlens/issues/4)
and explain your use case.
Also you can add command `dart run layerlens --fail-on-cycles` to the repo's pre-submit bots.

## Supported languages

Expand All @@ -66,3 +75,9 @@ See [`CONTRIBUTING.md`](CONTRIBUTING.md) for details.
## License

Apache 2.0; see [`LICENSE`](LICENSE) for details.

## Disclaimer

This project is not an official Google project. It is not supported by
Google and Google specifically disclaims all warranties as to its quality,
merchantability, or fitness for a particular purpose.
49 changes: 46 additions & 3 deletions bin/layerlens.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import 'package:layerlens/layerlens.dart';
enum _Options {
path('path'),
package('package'),
usage('usage'),
help('help'),
failOnCycles('fail-on-cycles'),
;

const _Options(this.name);
Expand All @@ -27,18 +30,58 @@ enum _Options {

void main(List<String> args) async {
final parser = ArgParser()
..addOption(_Options.path.name, defaultsTo: '.')
..addFlag(
_Options.usage.name,
defaultsTo: false,
help:
'Prints help on how to use the command. The same as --${_Options.usage.name}.',
)
..addFlag(
_Options.help.name,
defaultsTo: false,
help:
'Prints help on how to use the command. The same as --${_Options.help.name}.',
)
..addFlag(
_Options.failOnCycles.name,
defaultsTo: false,
help: 'Fail if there are circular dependencies.',
)
..addOption(
_Options.path.name,
defaultsTo: '.',
help: 'Root directory of the package.',
)
..addOption(
_Options.package.name,
defaultsTo: null,
help: 'Package name is needed when internal '
help: 'Package name, that is needed when internal '
'libraries reference each other with `package:` import.',
);

final parsedArgs = parser.parse(args);
late final ArgResults parsedArgs;

try {
parsedArgs = parser.parse(args);
} on FormatException catch (e) {
print(e.message);
print(parser.usage);
return;
}

if (parsedArgs[_Options.usage.name] == true ||
parsedArgs[_Options.help.name] == true) {
print(parser.usage);
return;
}

final generatedDiagrams = await generateLayering(
rootDir: parsedArgs[_Options.path.name],
packageName: parsedArgs[_Options.package.name],
failOnCycles: parsedArgs[_Options.failOnCycles.name] as bool,
cyclesFailureMessage: '''Error: cycles detected.
To see the cycles, generate diagrams without --${_Options.failOnCycles.name} and search for '--!--'.
''',
);
print(
'Generated $generatedDiagrams diagrams. Check files DEPENDENCIES.md in source folders.',
Expand Down
30 changes: 28 additions & 2 deletions lib/layerlens.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,50 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import 'dart:io';

import 'src/generator.dart';
import 'src/analyzer.dart';

import 'src/code_parser.dart';
import 'package:meta/meta.dart';

typedef ExitFn = Function(int code);

/// Generates dependency diagram in eash source folder
/// where dart libraries or folders depend on eash other.
/// Generates dependency diagram in each source folder
/// where there are dependencies between dart libraries or folders.
///
/// Returns number of generated diagrams.
Future<int> generateLayering({
required String rootDir,
required String? packageName,
required bool failOnCycles,
required String cyclesFailureMessage,
ExitFn exitFn = exit,
}) async {
final deps = await collectDeps(
rootDir: rootDir,
packageName: packageName,
);
final layering = Analyzer(deps);
handleCycles(
layering,
exitFn,
failOnCycles: failOnCycles,
failureMessage: cyclesFailureMessage,
);
return await MdGenerator(sourceFolder: layering.root, rootDir: rootDir)
.generateFiles();
}

@visibleForTesting
void handleCycles(
Analyzer layering,
ExitFn exitFn, {
required bool failOnCycles,
required String failureMessage,
}) {
if (layering.root.totalInversions == 0 || !failOnCycles) return;
print(failureMessage);
exitFn(1);
}
5 changes: 3 additions & 2 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: layerlens
version: 1.0.15
version: 1.0.16
description: Generate a dependency diagram in every folder of your source code.
repository: https://github.com/polina-c/layerlens

Expand All @@ -10,11 +10,12 @@ environment:
sdk: '>=3.0.0 <4.0.0'

dependencies:
analyzer: ^5.4.0
analyzer: '>=5.4.0 <7.0.0'
args: ^2.3.1
platform: ^3.1.0
path: ^1.8.2
surveyor: ^1.0.0-dev.2.0
meta: ^1.12.0

dev_dependencies:
lints: ^2.0.0
Expand Down
57 changes: 57 additions & 0 deletions test/layerlens_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright 2023 Google LLC
//
// 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
//
// https://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.

import 'package:layerlens/layerlens.dart';
import 'package:layerlens/src/analyzer.dart';
import 'package:layerlens/src/code_parser.dart';
import 'package:test/test.dart';

void main() async {
int? exitCodeUsed;
void exitMock(int code) {
exitCodeUsed = code;
}

final cycledLayering = {
true: await collectDeps(rootDir: 'example'),
false: await collectDeps(rootDir: '.'),
};

setUp(() {
exitCodeUsed = null;
});

for (final failOnCycles in [true, false]) {
for (final cycles in [true, false]) {
test(
'handleCycles exits for cycles with flag, cycles=$cycles, failOnCycles=$failOnCycles',
() async {
final layering = Analyzer(cycledLayering[cycles]!);
handleCycles(
layering,
exitMock,
failOnCycles: failOnCycles,
failureMessage: 'Cycles found',
);

bool shouldExit = failOnCycles && cycles;
if (shouldExit) {
expect(exitCodeUsed, 1);
} else {
expect(exitCodeUsed, null);
}
});
}
}
}

0 comments on commit 33b200e

Please sign in to comment.