Skip to content

Commit

Permalink
[rush-sdk] Expose file change analysis on the ProjectChangeAnalyzer
Browse files Browse the repository at this point in the history
… class (#4959)

* Expose analysis of file changes using ProjectChangeAnalyzer.getChangedFilesAsync

* Rush change

* Add a hook to allow modifying file change analysis

* Update change

* Revert index changes

* Update API

* Update API

* Fix issue with type export

* Rush update

* Make generic

* PR feedback

* Move helper function into @rushstack/lookup-by-path

* Rush change

* Fix an issue with grouping when the entries are falsy

---------

Co-authored-by: Daniel <[email protected]>
  • Loading branch information
D4N14L and D4N14L authored Oct 3, 2024
1 parent 1d8f323 commit 0d2d532
Show file tree
Hide file tree
Showing 11 changed files with 203 additions and 64 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@microsoft/rush",
"comment": "Expose `getChangesByProject` to allow classes that extend ProjectChangeAnalyzer to override file change analysis",
"type": "patch"
}
],
"packageName": "@microsoft/rush"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@rushstack/lookup-by-path",
"comment": "Allow for a map of file paths to arbitrary info to be grouped by the nearest entry in the LookupByPath trie",
"type": "minor"
}
],
"packageName": "@rushstack/lookup-by-path"
}
25 changes: 13 additions & 12 deletions common/config/subspaces/build-tests-subspace/pnpm-lock.yaml

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

4 changes: 2 additions & 2 deletions common/config/subspaces/build-tests-subspace/repo-state.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush.
{
"pnpmShrinkwrapHash": "0e569d956f72f98565ea4207cba2d7b359e5cdaa",
"pnpmShrinkwrapHash": "5b75a8ef91af53a8caf52319e5eb0042c4d06852",
"preferredVersionsHash": "ce857ea0536b894ec8f346aaea08cfd85a5af648",
"packageJsonInjectedDependenciesHash": "15081ac6b4174f98e6a82a839055fbda1a33680d"
"packageJsonInjectedDependenciesHash": "8927ca4e0147b9436659f98a2ff8ca347107d52f"
}
3 changes: 3 additions & 0 deletions common/config/subspaces/default/pnpm-lock.yaml

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

1 change: 1 addition & 0 deletions common/reviews/api/lookup-by-path.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export class LookupByPath<TItem> {
findChildPath(childPath: string): TItem | undefined;
findChildPathFromSegments(childPathSegments: Iterable<string>): TItem | undefined;
findLongestPrefixMatch(query: string): IPrefixMatch<TItem> | undefined;
groupByChild<TInfo>(infoByPath: Map<string, TInfo>): Map<TItem, Map<string, TInfo>>;
static iteratePathSegments(serializedPath: string, delimiter?: string): Iterable<string>;
setItem(serializedPath: string, value: TItem): this;
setItemFromSegments(pathSegments: Iterable<string>, value: TItem): this;
Expand Down
3 changes: 3 additions & 0 deletions common/reviews/api/rush-lib.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { CollatedWriter } from '@rushstack/stream-collator';
import type { CommandLineParameter } from '@rushstack/ts-command-line';
import { CommandLineParameterKind } from '@rushstack/ts-command-line';
import { HookMap } from 'tapable';
import { IFileDiffStatus } from '@rushstack/package-deps-hash';
import { IPackageJson } from '@rushstack/node-core-library';
import { IPrefixMatch } from '@rushstack/lookup-by-path';
import { ITerminal } from '@rushstack/terminal';
Expand Down Expand Up @@ -1116,6 +1117,8 @@ export class ProjectChangeAnalyzer {
// (undocumented)
_filterProjectDataAsync<T>(project: RushConfigurationProject, unfilteredProjectData: Map<string, T>, rootDir: string, terminal: ITerminal): Promise<Map<string, T>>;
getChangedProjectsAsync(options: IGetChangedProjectsOptions): Promise<Set<RushConfigurationProject>>;
// (undocumented)
protected getChangesByProject(lookup: LookupByPath<RushConfigurationProject>, changedFiles: Map<string, IFileDiffStatus>): Map<RushConfigurationProject, Map<string, IFileDiffStatus>>;
// @internal
_tryGetProjectDependenciesAsync(project: RushConfigurationProject, terminal: ITerminal): Promise<Map<string, string> | undefined>;
// @internal
Expand Down
27 changes: 27 additions & 0 deletions libraries/lookup-by-path/src/LookupByPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,33 @@ export class LookupByPath<TItem> {
return best;
}

/**
* Groups the provided map of info by the nearest entry in the trie that contains the path. If the path
* is not found in the trie, the info is ignored.
*
* @returns The grouped info, grouped by the nearest entry in the trie that contains the path
*
* @param infoByPath - The info to be grouped, keyed by path
*/
public groupByChild<TInfo>(infoByPath: Map<string, TInfo>): Map<TItem, Map<string, TInfo>> {
const groupedInfoByChild: Map<TItem, Map<string, TInfo>> = new Map();

for (const [path, info] of infoByPath) {
const child: TItem | undefined = this.findChildPath(path);
if (child === undefined) {
continue;
}
let groupedInfo: Map<string, TInfo> | undefined = groupedInfoByChild.get(child);
if (!groupedInfo) {
groupedInfo = new Map();
groupedInfoByChild.set(child, groupedInfo);
}
groupedInfo.set(path, info);
}

return groupedInfoByChild;
}

/**
* Iterates through progressively longer prefixes of a given string and returns as soon
* as the number of candidate items that match the prefix are 1 or 0.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import { LookupByPath } from './LookupByPath';
import { LookupByPath } from '../LookupByPath';

describe(LookupByPath.iteratePathSegments.name, () => {
it('returns empty for an empty string', () => {
Expand Down Expand Up @@ -143,3 +143,92 @@ describe(LookupByPath.prototype.findLongestPrefixMatch.name, () => {
expect(tree.findLongestPrefixMatch('foo/foo')).toEqual({ value: 1, index: 3 });
});
});

describe(LookupByPath.prototype.groupByChild.name, () => {
const lookup: LookupByPath<string> = new LookupByPath([
['foo', 'foo'],
['foo/bar', 'bar'],
['foo/bar/baz', 'baz']
]);

it('returns empty map for empty input', () => {
expect(lookup.groupByChild(new Map())).toEqual(new Map());
});

it('groups items by the closest group that contains the file path', () => {
const infoByPath: Map<string, string> = new Map([
['foo', 'foo'],
['foo/bar', 'bar'],
['foo/bar/baz', 'baz'],
['foo/bar/baz/qux', 'qux'],
['foo/bar/baz/qux/quux', 'quux']
]);

const expected: Map<string, Map<string, string>> = new Map([
['foo', new Map([['foo', 'foo']])],
['bar', new Map([['foo/bar', 'bar']])],
[
'baz',
new Map([
['foo/bar/baz', 'baz'],
['foo/bar/baz/qux', 'qux'],
['foo/bar/baz/qux/quux', 'quux']
])
]
]);

expect(lookup.groupByChild(infoByPath)).toEqual(expected);
});

it('ignores items that do not exist in the lookup', () => {
const infoByPath: Map<string, string> = new Map([
['foo', 'foo'],
['foo/qux', 'qux'],
['bar', 'bar'],
['baz', 'baz']
]);

const expected: Map<string, Map<string, string>> = new Map([
[
'foo',
new Map([
['foo', 'foo'],
['foo/qux', 'qux']
])
]
]);

expect(lookup.groupByChild(infoByPath)).toEqual(expected);
});

it('ignores items that do not exist in the lookup when the lookup children are possibly falsy', () => {
const falsyLookup: LookupByPath<string> = new LookupByPath([
['foo', 'foo'],
['foo/bar', 'bar'],
['foo/bar/baz', '']
]);

const infoByPath: Map<string, string> = new Map([
['foo', 'foo'],
['foo/bar', 'bar'],
['foo/bar/baz', 'baz'],
['foo/bar/baz/qux', 'qux'],
['foo/bar/baz/qux/quux', 'quux']
]);

const expected: Map<string, Map<string, string>> = new Map([
['foo', new Map([['foo', 'foo']])],
['bar', new Map([['foo/bar', 'bar']])],
[
'',
new Map([
['foo/bar/baz', 'baz'],
['foo/bar/baz/qux', 'qux'],
['foo/bar/baz/qux/quux', 'quux']
])
]
]);

expect(falsyLookup.groupByChild(infoByPath)).toEqual(expected);
});
});
Loading

0 comments on commit 0d2d532

Please sign in to comment.