Skip to content

Commit c3e639b

Browse files
FIX (CodeAnalyzer): @W-16121713@: Allow project to live under .dot folders (#1530)
1 parent 2bbd619 commit c3e639b

File tree

6 files changed

+95
-16
lines changed

6 files changed

+95
-16
lines changed

src/lib/DefaultRuleManager.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export class DefaultRuleManager implements RuleManager {
7474
return this.catalog.getRulesMatchingFilters(filters);
7575
}
7676

77-
async runRulesMatchingCriteria(filters: RuleFilter[], targets: string[], runOptions: RunOptions, engineOptions: EngineOptions): Promise<Results> {
77+
async runRulesMatchingCriteria(filters: RuleFilter[], targets: string[], runOptions: RunOptions, engineOptions: EngineOptions, projectDir?: string): Promise<Results> {
7878
// Declare a variable that we can later use to store the engine results, as well as something to help us track
7979
// which engines actually ran.
8080
let ruleResults: RuleResult[] = [];
@@ -100,7 +100,7 @@ export class DefaultRuleManager implements RuleManager {
100100
// them all in. Note that some engines (pmd) need groups while others (eslint) need the rules.
101101
const engineGroups = ruleGroups.filter(g => g.engine === e.getName());
102102
const engineRules = rules.filter(r => r.engine === e.getName());
103-
const engineTargets = await this.unpackTargets(e, targets, matchedTargets);
103+
const engineTargets = await this.unpackTargets(e, targets, matchedTargets, projectDir);
104104
this.logger.trace(`For ${e.getName()}, found ${engineGroups.length} groups, ${engineRules.length} rules, ${engineTargets.length} targets`);
105105

106106
if (e.shouldEngineRun(engineGroups, engineRules, engineTargets, engineOptions)) {
@@ -247,7 +247,7 @@ export class DefaultRuleManager implements RuleManager {
247247
*
248248
* Any items from the 'targets' array that result in a match are added to the 'matchedTargets' Set.
249249
*/
250-
protected async unpackTargets(engine: RuleEngine, targets: string[], matchedTargets: Set<string>): Promise<RuleTarget[]> {
250+
protected async unpackTargets(engine: RuleEngine, targets: string[], matchedTargets: Set<string>, projectDir?: string): Promise<RuleTarget[]> {
251251
const ruleTargets: RuleTarget[] = [];
252252
// Ask engines for their desired target patterns.
253253
const engineTargets = await engine.getTargetPatterns();
@@ -283,7 +283,7 @@ export class DefaultRuleManager implements RuleManager {
283283

284284
// We want to use a path matcher that can filter based on the engine's target patterns and any negative globs
285285
// provided to us.
286-
const pm = new PathMatcher([...engineTargets, ...negativePatterns]);
286+
const pm = new PathMatcher([...engineTargets, ...negativePatterns], projectDir);
287287
for (const target of positivePatterns) {
288288
// Used to detect if the target resulted in a match
289289
const ruleTargetsInitialLength: number = ruleTargets.length;

src/lib/InputProcessor.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export interface InputProcessor {
2121

2222
resolveTargetPaths(inputs: Inputs): string[];
2323

24-
resolveProjectDirPaths(inputs: Inputs): string[];
24+
resolveProjectDirPaths(inputs: Inputs, displayResolvedProjectDir?: boolean): string[];
2525

2626
createRunOptions(inputs: Inputs, isDfa: boolean): RunOptions;
2727

@@ -45,7 +45,7 @@ export class InputProcessorImpl implements InputProcessor {
4545
return (inputs.path as string[]).map(p => path.resolve(untildify(p)));
4646
}
4747

48-
public resolveProjectDirPaths(inputs: Inputs): string[] {
48+
public resolveProjectDirPaths(inputs: Inputs, displayResolvedProjectDir: boolean = true): string[] {
4949
// If projectdir is provided, then return it since at this point it has already been validated to exist
5050
if (inputs.projectdir && (inputs.projectdir as string[]).length > 0) {
5151
return (inputs.projectdir as string[]).map(p => path.resolve(normalize(untildify(p))))
@@ -58,7 +58,9 @@ export class InputProcessorImpl implements InputProcessor {
5858
const commonParentFolder = getFirstCommonParentFolder(this.getAllTargetFiles(inputs));
5959
let projectFolder: string = findFolderThatContainsSfdxProjectFile(commonParentFolder);
6060
projectFolder = projectFolder.length > 0 ? projectFolder : commonParentFolder
61-
this.displayInfoOnlyOnce('info.resolvedProjectDir', [projectFolder])
61+
if (displayResolvedProjectDir) {
62+
this.displayInfoOnlyOnce('info.resolvedProjectDir', [projectFolder])
63+
}
6264
return [projectFolder];
6365
}
6466

src/lib/RuleManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,5 @@ export interface RuleManager {
2929
/**
3030
* @param engineOptions - see RuleEngine#run
3131
*/
32-
runRulesMatchingCriteria(filters: RuleFilter[], target: string[], runOptions: RunOptions, engineOptions: EngineOptions): Promise<Results>;
32+
runRulesMatchingCriteria(filters: RuleFilter[], target: string[], runOptions: RunOptions, engineOptions: EngineOptions, projectDir?: string): Promise<Results>;
3333
}

src/lib/actions/AbstractRunAction.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,13 @@ export abstract class AbstractRunAction implements Action {
9090
const resultsProcessor: ResultsProcessor = this.resultsProcessorFactory.createResultsProcessor(
9191
this.display, outputOptions, jsonReturnValueHolder);
9292

93+
const primaryProjectDir: string = this.inputProcessor.resolveProjectDirPaths(inputs, false)[0];
94+
9395
// TODO: Inject RuleManager as a dependency to improve testability by removing coupling to runtime implementation
9496
const ruleManager: RuleManager = await Controller.createRuleManager();
9597

9698
try {
97-
const results: Results = await ruleManager.runRulesMatchingCriteria(filters, targetPaths, runOptions, engineOptions);
99+
const results: Results = await ruleManager.runRulesMatchingCriteria(filters, targetPaths, runOptions, engineOptions, primaryProjectDir);
98100
this.logger.trace(`Processing output with format ${outputOptions.format}`);
99101
await resultsProcessor.processResults(results);
100102
return jsonReturnValueHolder.get();

src/lib/util/PathMatcher.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,17 @@ type SortedPatterns = {
1010

1111
export class PathMatcher {
1212
private readonly matcher: TargetMatchingFunction;
13+
private readonly normalizedProjectDir: string;
1314

1415
/**
1516
*
1617
* @param {TargetPattern[]} patterns - An array of TargetPatterns against which paths can be compared. Patterns can
1718
* be positive or negative strings (i.e. don't/do start with `!`), or objects
1819
* describing complex patterns.
1920
*/
20-
constructor(patterns: TargetPattern[]) {
21+
constructor(patterns: TargetPattern[], projectDir?: string) {
2122
this.matcher = this.generateMatchingFunction(patterns);
23+
this.normalizedProjectDir = normalize(projectDir || process.cwd());
2224
}
2325

2426

@@ -29,8 +31,8 @@ export class PathMatcher {
2931
private generateMatchingFunction(patterns: TargetPattern[]): TargetMatchingFunction {
3032
const {inclusionPatterns, exclusionPatterns, advancedPatterns} = this.sortPatterns(patterns);
3133

32-
const inclusionMatcher = picomatch(inclusionPatterns);
33-
const exclusionMatcher = picomatch(exclusionPatterns);
34+
const inclusionMatcher = picomatch(inclusionPatterns, {dot: true});
35+
const exclusionMatcher = picomatch(exclusionPatterns, {dot: true});
3436
// Each of the advanced patterns generates its own matching function, which will be applied in sequence.
3537
const advancedMatchers = advancedPatterns.map(ap => this.generateAdvancedMatcher(ap));
3638

@@ -115,7 +117,7 @@ export class PathMatcher {
115117
* @returns {Promise<string[]>} - The subset of the target strings that match the provided patterns.
116118
*/
117119
public async filterPathsByPatterns(targets: string[]): Promise<string[]> {
118-
const matchResults: boolean[] = await Promise.all(targets.map(t => this.matcher(normalize(t))));
120+
const matchResults: boolean[] = await Promise.all(targets.map(t => this.pathMatchesPatterns(t)));
119121

120122
const filteredTargets: string[] = [];
121123
matchResults.forEach((r: boolean, idx: number) => {
@@ -132,6 +134,19 @@ export class PathMatcher {
132134
* @returns {Promise<boolean>}
133135
*/
134136
public async pathMatchesPatterns(target: string): Promise<boolean> {
135-
return this.matcher(normalize(target));
137+
return (await this.matcher(normalize(target))) && !this.isDotFileOrIsInDotFolderUnderProjectDir(target);
138+
}
139+
140+
/**
141+
* Returns true if the target is a .fileName or is in a .folder under the project directory
142+
* Note that we do not want to exclude typical files underneath the projectDir just because the projectDir is
143+
* underneath a dot folder (like .jenkins/projectFolder/...), so we had to add {dot: true} to the picomatch
144+
* above. But we still want to exclude dot files and dot folders underneath the projectDir.
145+
*/
146+
private isDotFileOrIsInDotFolderUnderProjectDir(target: string): boolean {
147+
if (target.startsWith(this.normalizedProjectDir)) {
148+
target = target.slice(this.normalizedProjectDir.length);
149+
}
150+
return target.includes('/.');
136151
}
137152
}

test/lib/util/PathMatcher.test.ts

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,34 @@ describe('PathMatcher', () => {
164164
expect(results).to.not.include(targets[4], 'Path wrongly included');
165165
});
166166
});
167+
168+
describe('Hidden dot folders', () => {
169+
it('When projectDir contains a .dotFolder then the files underneath it are not matched', async () => {
170+
const pm: PathMatcher = new PathMatcher(['**/*.js', '/some/projectFolder/**/*.cls'], '/some/projectFolder');
171+
const targets: string[] = [
172+
'/some/projectFolder/subFolder/.dotFolder/a.js',
173+
'/some/projectFolder/subFolder/nonDotFolder/b.js',
174+
'/some/projectFolder/subFolder/.dotFolder/c.cls',
175+
'/some/projectFolder/subFolder/nonDotFolder/d.cls'
176+
];
177+
178+
const results: string[] = await pm.filterPathsByPatterns(targets);
179+
expect(results).to.deep.equal([targets[1],targets[3]]);
180+
});
181+
182+
it('When projectDir is under a .dotFolder then the projectDir is not excluded', async () => {
183+
const pm: PathMatcher = new PathMatcher(['**/*.js', '/some/.dotFolder/projectFolder/**/*.cls'], '/some/.dotFolder/projectFolder');
184+
const targets: string[] = [
185+
'/some/.dotFolder/projectFolder/subFolder/.dotFolder/a.js',
186+
'/some/.dotFolder/projectFolder/subFolder/nonDotFolder/b.js',
187+
'/some/.dotFolder/projectFolder/subFolder/.dotFolder/c.cls',
188+
'/some/.dotFolder/projectFolder/subFolder/nonDotFolder/d.cls'
189+
];
190+
191+
const results: string[] = await pm.filterPathsByPatterns(targets);
192+
expect(results).to.deep.equal([targets[1],targets[3]]);
193+
});
194+
});
167195
});
168196

169197
describe('#pathMatchesPatterns()', () => {
@@ -263,6 +291,38 @@ describe('PathMatcher', () => {
263291
it('DOES NOT MATCH paths matching negative patterns but not positive patterns', async () => {
264292
expect(await pm.pathMatchesPatterns(targets[4])).to.equal(false, 'Path wrongly not matched.');
265293
});
266-
})
267-
})
294+
});
295+
296+
describe('Hidden dot folders', () => {
297+
it('When projectDir contains a .dotFolder then the files underneath it are not matched', async () => {
298+
const pm: PathMatcher = new PathMatcher(['**/*.js', '/some/projectFolder/**/*.cls'], '/some/projectFolder');
299+
const targets: string[] = [
300+
'/some/projectFolder/subFolder/.dotFolder/a.js',
301+
'/some/projectFolder/subFolder/nonDotFolder/b.js',
302+
'/some/projectFolder/subFolder/.dotFolder/c.cls',
303+
'/some/projectFolder/subFolder/nonDotFolder/d.cls'
304+
];
305+
306+
expect(await pm.pathMatchesPatterns(targets[0])).to.equal(false);
307+
expect(await pm.pathMatchesPatterns(targets[1])).to.equal(true);
308+
expect(await pm.pathMatchesPatterns(targets[2])).to.equal(false);
309+
expect(await pm.pathMatchesPatterns(targets[3])).to.equal(true);
310+
});
311+
312+
it('When projectDir is under a .dotFolder then the projectDir is not excluded', async () => {
313+
const pm: PathMatcher = new PathMatcher(['**/*.js', '/some/.dotFolder/projectFolder/**/*.cls'], '/some/.dotFolder/projectFolder/');
314+
const targets: string[] = [
315+
'/some/.dotFolder/projectFolder/.dotFolder/a.js',
316+
'/some/.dotFolder/projectFolder/nonDotFolder/b.js',
317+
'/some/.dotFolder/projectFolder/.dotFolder/c.cls',
318+
'/some/.dotFolder/projectFolder/nonDotFolder/d.cls'
319+
];
320+
321+
expect(await pm.pathMatchesPatterns(targets[0])).to.equal(false);
322+
expect(await pm.pathMatchesPatterns(targets[1])).to.equal(true);
323+
expect(await pm.pathMatchesPatterns(targets[2])).to.equal(false);
324+
expect(await pm.pathMatchesPatterns(targets[3])).to.equal(true);
325+
});
326+
});
327+
});
268328
});

0 commit comments

Comments
 (0)