Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): support allSourceTags (#768) and wildcards in check-module-boundaries.js #771

Merged
merged 2 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
feat(core): add regex support
  • Loading branch information
Tungsten78 committed Oct 10, 2023
commit 56d4f2a85e8af7eba5c4ce5965d95e766b6d5caa
44 changes: 34 additions & 10 deletions packages/core/src/tasks/check-module-boundaries.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,20 @@ const MOCK_BOUNDARIES: ModuleBoundaries = [
sourceTag: 'no-deps',
},
{
onlyDependOnLibsWithTags: [],
allSourceTags: ['--*--'],
onlyDependOnLibsWithTags: ['*baz*'],
allSourceTags: ['--f*o--'],
},
{
onlyDependOnLibsWithTags: [],
sourceTag: '--*--',
notDependOnLibsWithTags: ['b*z'],
sourceTag: '--f*o--',
},
{
onlyDependOnLibsWithTags: ['*baz*'],
allSourceTags: ['--foo--'],
onlyDependOnLibsWithTags: ['/.*baz.*$/'],
allSourceTags: ['/^==f[o]{2}==$/'],
},
{
notDependOnLibsWithTags: ['b*z'],
sourceTag: '--foo--',
notDependOnLibsWithTags: ['/^b.*z$/'],
sourceTag: '/^==f[o]{2}==$/',
},
];

Expand Down Expand Up @@ -313,7 +313,7 @@ describe('enforce-module-boundaries', () => {
expect(results).toHaveLength(1);
});

it('should support wildcards', async () => {
it('should support glob wildcards', async () => {
const globResults = ['libs/x/x.csproj'];
jest.spyOn(fastGlob, 'sync').mockImplementation(() => globResults);

Expand All @@ -334,6 +334,30 @@ describe('enforce-module-boundaries', () => {
root: 'libs/a',
},
});
expect(results).toHaveLength(4);
expect(results).toHaveLength(2);
});

it('should support regexp', async () => {
const globResults = ['libs/x/x.csproj'];
jest.spyOn(fastGlob, 'sync').mockImplementation(() => globResults);

vol.fromJSON({
'libs/x/x.csproj':
'<Project Sdk="Microsoft.NET.Sdk.Web"><ItemGroup><ProjectReference Include="..\\..\\libs\\a\\a.csproj" /></ItemGroup></Project>',
});

const results = await checkModuleBoundariesForProject('x', {
x: {
tags: ['==foo=='],
targets: { a: {} },
root: 'libs/x',
},
a: {
tags: ['biz'],
targets: {},
root: 'libs/a',
},
});
expect(results).toHaveLength(2);
});
});
111 changes: 58 additions & 53 deletions packages/core/src/tasks/check-module-boundaries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { relative } from 'path';
import {
getDependantProjectsForNxProject,
ModuleBoundaries,
ModuleBoundary,
readConfig,
} from '@nx-dotnet/utils';

Expand All @@ -24,16 +25,8 @@ export async function checkModuleBoundariesForProject(
if (!tags.length) {
return [];
}
const configuredConstraints = await loadModuleBoundaries(projectRoot);
const relevantConstraints = configuredConstraints.filter(
(x) =>
((x.sourceTag && tags.includesWithWildcards(x.sourceTag)) ||
(x.allSourceTags &&
x.allSourceTags.every((tag) => tags.includesWithWildcards(tag)))) &&
(!x.onlyDependOnLibsWithTags?.includes('*') ||
x.notDependOnLibsWithTags?.length),
);
if (!relevantConstraints.length) {
const constraints = await getProjectConstraints(projectRoot, tags);
if (!constraints.length) {
return [];
}

Expand All @@ -44,15 +37,8 @@ export async function checkModuleBoundariesForProject(
(configuration, name, implicit) => {
if (implicit) return;
const dependencyTags = configuration?.tags ?? [];
for (const constraint of relevantConstraints) {
if (
!dependencyTags.some((x) =>
constraint.onlyDependOnLibsWithTags?.includesWithWildcards(x),
) ||
dependencyTags.some((x) =>
constraint.notDependOnLibsWithTags?.includesWithWildcards(x),
)
) {
for (const constraint of constraints) {
if (hasConstraintViolation(constraint, dependencyTags)) {
violations.push(
`${project} cannot depend on ${name}. Project tag ${JSON.stringify(
constraint,
Expand All @@ -65,6 +51,30 @@ export async function checkModuleBoundariesForProject(
return violations;
}

async function getProjectConstraints(root: string, tags: string[]) {
const configuredConstraints = await loadModuleBoundaries(root);
return configuredConstraints.filter(
(x) =>
((x.sourceTag && hasMatch(tags, x.sourceTag)) ||
x.allSourceTags?.every((tag) => hasMatch(tags, tag))) &&
(!x.onlyDependOnLibsWithTags?.includes('*') ||
x.notDependOnLibsWithTags?.length),
);
}

function hasConstraintViolation(
constraint: ModuleBoundary,
dependencyTags: string[],
) {
return (
!dependencyTags.some((x) =>
hasMatch(constraint.onlyDependOnLibsWithTags ?? [], x),
) ||
dependencyTags.some((x) =>
hasMatch(constraint.notDependOnLibsWithTags ?? [], x),
)
);
}
/**
* Loads module boundaries from eslintrc or .nx-dotnet.rc.json
* @param root Which file should be used when pulling from eslint
Expand Down Expand Up @@ -122,45 +132,40 @@ function findProjectGivenRoot(
}
}

function includesWithWildcards(input: string, pattern: string): boolean {
if (pattern.includes('*')) {
const searchParts = pattern.split('*');
let lastIndex = 0;
for (const part of searchParts) {
const index = input.indexOf(part, lastIndex);
if (index === -1) {
return false;
}
lastIndex = index + part.length;
const regexMap = new Map<string, RegExp>();

function hasMatch(input: string[], pattern: string): boolean {
if (pattern === '*') return true;

// if the pattern is a regex, check if any of the input strings match the regex
if (pattern.startsWith('/') && pattern.endsWith('/')) {
let regex = regexMap.get(pattern);
if (!regex) {
regex = new RegExp(pattern.substring(1, pattern.length - 1));
regexMap.set(pattern, regex);
}
return true;
} else {
return input.includes(pattern);
return input.some((t) => regex?.test(t));
}
}

declare global {
interface String {
includesWithWildcards(pattern: string): boolean;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Array<T> {
includesWithWildcards(pattern: string): boolean;
// if the pattern is a glob, check if any of the input strings match the glob prefix
if (pattern.includes('*')) {
const regex = mapGlobToRegExp(pattern);
return input.some((t) => regex.test(t));
}

return input.indexOf(pattern) > -1;
}

/**
* Maps import with wildcards to regex pattern
* @param importDefinition
* @returns
*/
function mapGlobToRegExp(importDefinition: string): RegExp {
// we replace all instances of `*`, `**..*` and `.*` with `.*`
const mappedWildcards = importDefinition.split(/(?:\.\*)|\*+/).join('.*');
return new RegExp(`^${new RegExp(mappedWildcards).source}$`);
}
String.prototype.includesWithWildcards = function (
this: string,
pattern: string,
): boolean {
return includesWithWildcards(this, pattern);
};

Array.prototype.includesWithWildcards = function (
this: string[],
pattern: string,
): boolean {
return this.some((x) => includesWithWildcards(x, pattern));
};

async function main() {
const parser = await import('yargs-parser');
Expand Down
6 changes: 4 additions & 2 deletions packages/utils/src/lib/models/nx.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export type ModuleBoundaries = {
export type ModuleBoundary = {
sourceTag?: '*' | string;
allSourceTags?: string[];
onlyDependOnLibsWithTags?: string[];
notDependOnLibsWithTags?: string[];
}[];
};

export type ModuleBoundaries = ModuleBoundary[];