Skip to content

Commit

Permalink
feat(extract): adds first sketch to get basic stats from the parsed A…
Browse files Browse the repository at this point in the history
…ST's (#926)

## Description

- adds basic stats per module, derived from the AST

## Motivation and Context

- will help identify barrels (and enable defining rules on them)
- curiosity

## How Has This Been Tested?

- [x] green ci
- [x] additional automated non-regression tests

## Types of changes

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] Documentation only change
- [ ] Refactor (non-breaking change which fixes an issue without
changing functionality)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
  • Loading branch information
sverweij authored Apr 6, 2024
1 parent 8c71ca4 commit 2bead21
Show file tree
Hide file tree
Showing 38 changed files with 479 additions and 20 deletions.
4 changes: 3 additions & 1 deletion .dependency-cruiser.json
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,9 @@
// },
/* Experimental: the parser to use
*/
"parser": "tsc", // acorn, swc, tsc
"parser": "tsc", // acorn, tsc
"experimentalStats": true,
"metrics": true,
"enhancedResolveOptions": {
"exportsFields": ["exports"],
"aliasFields": ["browser"],
Expand Down
23 changes: 20 additions & 3 deletions doc/options-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
- [`builtInModules`: influencing what to consider built-in (/ core) modules](#builtinmodules-influencing-what-to-consider-built-in--core-modules)
- [enhancedResolveOptions](#enhancedresolveoptions)
- [forceDeriveDependents](#forcederivedependents)
- [experimentalStats](#experimentalstats)
- [parser](#parser)
- [cache](#cache)
- [progress](#progress)
Expand Down Expand Up @@ -1642,13 +1643,29 @@ Dependency-cruiser will automatically determine whether it needs to derive depen
However, if you want to force them to be derived, you can switch this variable
to `true`.
### `experimentalStats`
When set to true dependency-cruiser will emit an `experimentalStats` object
in the result for each module. This feature is not yet used by any of the
reporters dependency-cruiser ships with. The feature is also _experimental_
which means it might disappear or change in the future.
### `parser`
With this _EXPERIMENTAL_ feature you can specify which parser you want to use
as the primary parser: the `acorn` one, which handles all things javascript
(commonjs, es-modules, jsx), or one of two parser that can in addition parse
typescript; microsoft's `tsc` or the faster and smaller (but slightly less
feature rich) `swc`.
(commonjs, es-modules, jsx), or microsoft's `tsc` that can in addition parse
typescript. At this time it's still possible to pass `swc` here, but that value
is deprecated and will be removed in a future version.
```javascript
{
// ...
options: {
parser: "tsc";
}
}
```
`swc` and `tsc` only work when the compilers (respectively `@swc/core` and
`typescript`) are installed in the same spot as dependency-cruiser is. They're
Expand Down
6 changes: 6 additions & 0 deletions src/extract/acorn/extract-stats.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default function extractStats(pAST) {
return {
topLevelStatementCount: pAST?.body?.length || 0,
size: pAST?.end || 0,
};
}
6 changes: 6 additions & 0 deletions src/extract/acorn/extract.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import extractES6Deps from "./extract-es6-deps.mjs";
import extractCommonJSDeps from "./extract-cjs-deps.mjs";
import extractAMDDeps from "./extract-amd-deps.mjs";
import parse from "./parse.mjs";
import extractStats from "./extract-stats.mjs";

export function extract(
{ baseDir, moduleSystems, exoticRequireStrings },
Expand All @@ -24,3 +25,8 @@ export function extract(

return lDependencies;
}

export function getStats({ baseDir }, pFileName, pTranspileOptions) {
const lAST = parse.getASTCached(join(baseDir, pFileName), pTranspileOptions);
return extractStats(lAST);
}
6 changes: 3 additions & 3 deletions src/extract/extract-dependencies.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ function extractWithTsc(pCruiseOptions, pFileName, pTranspileOptions) {
function determineExtractionFunction(pCruiseOptions, pFileName) {
let lExtractionFunction = acornExtract;

if (swcShouldUse(pCruiseOptions, pFileName)) {
lExtractionFunction = swcExtract;
} else if (tscShouldUse(pCruiseOptions, pFileName)) {
if (tscShouldUse(pCruiseOptions, pFileName)) {
lExtractionFunction = extractWithTsc;
} else if (swcShouldUse(pCruiseOptions, pFileName)) {
lExtractionFunction = swcExtract;
}

return lExtractionFunction;
Expand Down
48 changes: 48 additions & 0 deletions src/extract/extract-stats.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {
getStats as tscStats,
shouldUse as tscShouldUse,
} from "./tsc/extract.mjs";
import { getStats as acornStats } from "./acorn/extract.mjs";

/**
* @param {IStrictCruiseOptions} pCruiseOptions
* @param {string} pFileName
* @returns {(IStrictCruiseOptions, string, any) => import("../../types/cruise-result.mjs").IDependency[]}
*/
function determineExtractionFunction(pCruiseOptions, pFileName) {
let lExtractionFunction = acornStats;

if (tscShouldUse(pCruiseOptions, pFileName)) {
lExtractionFunction = tscStats;
}

return lExtractionFunction;
}

/**
* Returns some stats for the module in pFileName
*
* @param {string} pFileName path to the file
* @param {import("../../types/dependency-cruiser.js").IStrictCruiseOptions} pCruiseOptions cruise options
* @param {import("../../types/dependency-cruiser.js").ITranspileOptions} pTranspileOptions an object with tsconfig ('typescript project') options
* ('flattened' so there's no need for file access on any
* 'extends' option in there)
* @return {import("../../types/dependency-cruiser.js").IDependency[]} an array of dependency objects (see above)
*/
export default function extractStats(
pFileName,
pCruiseOptions,
pTranspileOptions,
) {
try {
return determineExtractionFunction(pCruiseOptions, pFileName)(
pCruiseOptions,
pFileName,
pTranspileOptions,
);
} catch (pError) {
throw new Error(
`Extracting stats ran afoul of...\n\n ${pError.message}\n... in ${pFileName}\n\n`,
);
}
}
12 changes: 11 additions & 1 deletion src/extract/index.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import extractDependencies from "./extract-dependencies.mjs";
import extractStats from "./extract-stats.mjs";
import gatherInitialSources from "./gather-initial-sources.mjs";
import clearCaches from "./clear-caches.mjs";
import { bus } from "#utl/bus.mjs";

/* eslint max-params:0 */
/* eslint max-params:0 , max-lines-per-function:0*/
function extractRecursive(
pFileName,
pCruiseOptions,
Expand Down Expand Up @@ -46,6 +47,15 @@ function extractRecursive(
[
{
source: pFileName,
...(pCruiseOptions.experimentalStats
? {
experimentalStats: extractStats(
pFileName,
pCruiseOptions,
pTranspileOptions,
),
}
: {}),
dependencies: lDependencies,
},
],
Expand Down
6 changes: 6 additions & 0 deletions src/extract/tsc/extract-stats.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default function extractStats(pAST) {
return {
topLevelStatementCount: pAST?.statements?.length || 0,
size: pAST?.end || 0,
};
}
6 changes: 6 additions & 0 deletions src/extract/tsc/extract.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { join } from "node:path";
import { isTypeScriptCompatible } from "../helpers.mjs";
import extractTypeScriptDeps from "./extract-typescript-deps.mjs";
import parse from "./parse.mjs";
import extractStats from "./extract-stats.mjs";

export function shouldUse({ tsPreCompilationDeps, parser }, pFileName) {
return (
Expand All @@ -21,3 +22,8 @@ export function extract(
exoticRequireStrings,
).filter(({ moduleSystem }) => moduleSystems.includes(moduleSystem));
}

export function getStats({ baseDir }, pFileName, pTranspileOptions) {
const lAST = parse.getASTCached(join(baseDir, pFileName), pTranspileOptions);
return extractStats(lAST);
}
8 changes: 6 additions & 2 deletions src/schema/configuration.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/schema/configuration.schema.mjs

Large diffs are not rendered by default.

20 changes: 18 additions & 2 deletions src/schema/cruise-result.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/schema/cruise-result.schema.mjs

Large diffs are not rendered by default.

52 changes: 52 additions & 0 deletions test/extract/__fixtures__/with-stats/output.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
[
{
"source": "test/extract/__mocks__/with-stats/aap.mjs",
"experimentalStats": {
"topLevelStatementCount": 1,
"size": 36
},
"dependencies": [
{
"module": "./noot.mjs",
"moduleSystem": "es6",
"dynamic": false,
"exoticallyRequired": false,
"dependencyTypes": ["local", "import"],
"resolved": "test/extract/__mocks__/with-stats/noot.mjs",
"coreModule": false,
"followable": true,
"couldNotResolve": false,
"matchesDoNotFollow": false
}
]
},
{
"source": "test/extract/__mocks__/with-stats/noot.mjs",
"experimentalStats": {
"topLevelStatementCount": 2,
"size": 56
},
"dependencies": [
{
"module": "./mies.mjs",
"moduleSystem": "es6",
"dynamic": false,
"exoticallyRequired": false,
"dependencyTypes": ["local", "import"],
"resolved": "test/extract/__mocks__/with-stats/mies.mjs",
"coreModule": false,
"followable": true,
"couldNotResolve": false,
"matchesDoNotFollow": false
}
]
},
{
"source": "test/extract/__mocks__/with-stats/mies.mjs",
"experimentalStats": {
"topLevelStatementCount": 1,
"size": 20
},
"dependencies": []
}
]
37 changes: 37 additions & 0 deletions test/extract/__mocks__/extract-stats-testfile.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { deepEqual } from "node:assert/strict";
import extractStats from "#extract/acorn/extract-stats.mjs";
import { getStats } from "#extract/acorn/extract.mjs";

describe("[U] extract/acorn/extractStats", () => {
it("should return stats for a given AST", () => {
const lAST = {
body: [{ type: "FunctionDeclaration" }, { type: "VariableDeclaration" }],
end: 100,
};
deepEqual(extractStats(lAST), {
topLevelStatementCount: 2,
size: 100,
});
});

it("should return 0 for empty AST", () => {
deepEqual(extractStats({}), {
topLevelStatementCount: 0,
size: 0,
});
});
});

describe("[I] extract/acorn/extract - getStats", () => {
it("should return stats for a given file containing valid javascript", () => {
const lStats = getStats(
{ baseDir: "./test/extract/acorn" },
"extract-stats.spec.mjs",
{ extension: ".mjs" },
);
deepEqual(lStats, {
topLevelStatementCount: 5,
size: 1010,
});
});
});
1 change: 1 addition & 0 deletions test/extract/__mocks__/with-stats/aap.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import * as noot from "./noot.mjs";
1 change: 1 addition & 0 deletions test/extract/__mocks__/with-stats/mies.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 456;
2 changes: 2 additions & 0 deletions test/extract/__mocks__/with-stats/noot.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import * as mies from "./mies.mjs";
export default 123;
37 changes: 37 additions & 0 deletions test/extract/acorn/extract-stats.spec.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { deepEqual } from "node:assert/strict";
import extractStats from "#extract/acorn/extract-stats.mjs";
import { getStats } from "#extract/acorn/extract.mjs";

describe("[U] extract/acorn/extractStats", () => {
it("should return stats for a given AST", () => {
const lAST = {
body: [{ type: "FunctionDeclaration" }, { type: "VariableDeclaration" }],
end: 100,
};
deepEqual(extractStats(lAST), {
topLevelStatementCount: 2,
size: 100,
});
});

it("should return 0 for empty AST", () => {
deepEqual(extractStats({}), {
topLevelStatementCount: 0,
size: 0,
});
});
});

describe("[I] extract/acorn/extract - getStats", () => {
it("should return stats for a given file containing valid javascript", () => {
const lStats = getStats(
{ baseDir: "./test/extract/__mocks__" },
"extract-stats-testfile.mjs",
{ extension: ".mjs" },
);
deepEqual(lStats, {
topLevelStatementCount: 5,
size: 1010,
});
});
});
Loading

0 comments on commit 2bead21

Please sign in to comment.