Skip to content

Commit

Permalink
Transform patterns from relative form even if are not valid in FS
Browse files Browse the repository at this point in the history
The logic to process patterns to determine include paths is separated
from the logic to process patterns that remain as globs.

patterns that remain as globs are transformed from relative form to
absolute form and keeping the suffix, these don't need to be validated
against the file sytem.

include patters that become include paths need to be validated against
the file system.
Additionally when resolving include paths, don't transform include
patterns not given in relative form to absolute form, so the search
can be broad and only become specfic when in relative form.

Signed-off-by: Alvaro Sanchez-Leon <[email protected]>
  • Loading branch information
alvsan09 committed Apr 22, 2021
1 parent a7ffe88 commit 1adc8c1
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,23 @@ describe('ripgrep-search-in-workspace-server', function (): void {
});
ripgrepServer.setClient(client);
// Matching only the top 'test' folder and not any other 'test' subfolder
ripgrepServer.search(pattern, [rootDirAUri], { include: ['./test'], matchWholeWord: true });
});

it('should apply to all sub-folders of not relative pattern', done => {
const pattern = 'Copyright';

const client = new ResultAccumulator(() => {
const expected: SearchInWorkspaceExpectation[] = [
{ root: rootDirAUri, fileUri: 'small/test/test-spec.ts', line: 1, character: 5, length: pattern.length, lineText: '' },
{ root: rootDirAUri, fileUri: 'test/test-spec.ts', line: 1, character: 5, length: pattern.length, lineText: '' }
];

compareSearchResults(expected, client.results);
done();
});
ripgrepServer.setClient(client);
// Matching only the top 'test' folder and not any other 'test' subfolder
ripgrepServer.search(pattern, [rootDirAUri], { include: ['test'], matchWholeWord: true });
});

Expand Down Expand Up @@ -933,9 +950,10 @@ describe('ripgrep-search-in-workspace-server', function (): void {

describe('#resolvePatternToPathMap', function (): void {
this.timeout(10000);
it('should resolve pattern to path for filename', function (): void {
it('should not resolve paths from a not absolute / relative pattern', function (): void {
const pattern = 'carrots';
checkResolvedPathForPattern(pattern, path.join(rootDirA, pattern));
const resultMap = ripgrepServer['resolvePatternToPathsMap']([pattern], [rootDirA]);
expect(resultMap.size).equal(0);
});

it('should resolve pattern to path for relative filename', function (): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,57 +39,74 @@ function bytesOrTextToString(obj: IRgBytesOrText): string {
}

/**
* Attempts to build a valid absolute path from the given pattern and root folder.
* - If the first attempt is unsuccessful, retry without the glob suffix `/**`.
*
* @returns the valid path if found (with the suffix if indicated by @param keepSuffix).
* Joins the given root and pattern to form an absolute path
* as long as the pattern is in relative form e.g. './foo'.
*/
function findPathInSuffixedPattern(root: string, pattern: string, keepSuffix: boolean): string | undefined {
let searchPath = findPathInPattern(root, pattern);

if (searchPath) {
return searchPath;
function relativeToAbsolutePattern(root: string, pattern: string): string {
if (!isRelativeToBaseDirectory(pattern)) {
// No need to convert to absolute
return pattern;
}
return path.join(root, pattern);
}

// Retry without the glob suffix.
const { patternBase, suffix } = stripGlobSuffix(pattern);
if (suffix) {
searchPath = findPathInPattern(root, patternBase);
searchPath = keepSuffix && searchPath ? searchPath.concat(suffix) : searchPath;
}
return searchPath;
function isRelativeToBaseDirectory(filePath: string): boolean {
const normalizedPath = filePath.replace(/\\/g, '/');
return normalizedPath.startsWith('./');
}

function findPathInPattern(root: string, pattern: string): string | undefined {
const searchPath = path.join(root, pattern);
if (fs.existsSync(searchPath)) {
return searchPath;
/**
* Attempts to build a valid absolute file or directory from the given pattern and root folder.
* e.g. /a/b/c/foo/** to /a/b/c/foo, or './foo/**' to '${root}/foo'.
*
* @returns the valid path if found existing in the file system.
*/
function resolveIncludeFolderFromGlob(root: string, pattern: string): string | undefined {
const patternBase = stripGlobSuffix(pattern);

if (!path.isAbsolute(patternBase) && !isRelativeToBaseDirectory(patternBase)) {
// The include pattern is not referring to a single file / folder, i.e. not to be converted
// to include folder.
return undefined;
}

if (fs.existsSync(pattern)) {
return pattern;
const targetPath = path.isAbsolute(patternBase) ? patternBase : path.join(root, patternBase);

if (fs.existsSync(targetPath)) {
return targetPath;
}

return undefined;
}

/**
* Attempts to translate a pattern string prefixed with glob stars (e.g. /a/b/c/**)
* to a file system path (/a/b/c).
* Removes a glob suffix from a given pattern (e.g. /a/b/c/**)
* to a directory path (/a/b/c).
*
* @returns the new pattern without the glob prefix, else returns the original pattern if unsuccessful.
* @returns the path without the glob suffix,
* else returns the original pattern.
*/
function stripGlobSuffix(pattern: string): { patternBase: string, suffix: string | undefined } {
let patternBase = pattern;
const globAllSize = 3;
function stripGlobSuffix(pattern: string): string {
const pathParsed = path.parse(pattern);
const suffix = pathParsed.base;

return suffix === '**' ? pathParsed.dir : pattern;
}

const suffix = (pattern.length > globAllSize) ? pattern.slice(-globAllSize) : undefined;
/**
* Push an item to an existing string array only if the item is not already included.
* If the given array is undefined it creates a new one with the given item as the first entry.
*/
function pushIfNotIncluded(containerArray: string[] | undefined, item: string): string[] {
if (!containerArray) {
return [item];
}

if (suffix === '/**' || suffix === '\**') {
patternBase = pattern.slice(0, -globAllSize);
if (!containerArray.includes(item)) {
containerArray.push(item);
}

return { patternBase, suffix };
return containerArray;
}

type IRgMessage = IRgMatch | IRgBegin | IRgEnd;
Expand Down Expand Up @@ -244,7 +261,7 @@ export class RipgrepSearchInWorkspaceServer implements SearchInWorkspaceServer {
const searchId = this.nextSearchId++;
const rootPaths = rootUris.map(root => FileUri.fsPath(root));
const searchPaths: string[] = this.resolveSearchPathsFromIncludes(rootPaths, opts);
this.adjustExcludePaths(rootPaths, opts);
this.includesExcludesToAbsolute(searchPaths, opts);
const rgArgs = this.getArgs(opts);
// if we use matchWholeWord we use regExp internally,
// so, we need to escape regexp characters if we actually not set regexp true in UI.
Expand Down Expand Up @@ -403,22 +420,25 @@ export class RipgrepSearchInWorkspaceServer implements SearchInWorkspaceServer {
}

/**
* Transform exclude option patterns from relative paths to absolute paths as long
* as the resulting paths get validated.
* Transform include/exclude option patterns from relative patterns to absolute patterns.
* E.g. './abc/foo.*' to '${root}/abc/foo.*', the transformation does not validate the
* pattern against the file system as glob suffixes remain.
*/
protected adjustExcludePaths(rootPaths: string[], opts: SearchInWorkspaceOptions | undefined): void {
if (!opts || !opts.exclude) {
return;
}
protected includesExcludesToAbsolute(searchPaths: string[], opts: SearchInWorkspaceOptions | undefined): void {
[true, false].forEach(isInclude => {
const patterns = isInclude ? opts?.include : opts?.exclude;
if (!patterns) {
return;
}

const updatedPatterns = this.replaceRelativePatternsToAbsolute(patterns, searchPaths);

const patternToPathsMap: Map<string, string[]> = this.resolvePatternToPathsMap(opts.exclude, rootPaths, true);
const updatedExcludes: string[] = [];
opts.exclude.forEach(pattern => {
const maybePath = patternToPathsMap.get(pattern);
const adjustedPatterns = maybePath ?? [pattern];
updatedExcludes.push(...adjustedPatterns);
if (isInclude) {
opts!.include = updatedPatterns;
} else {
opts!.exclude = updatedPatterns;
}
});
opts.exclude = updatedExcludes;
}

/**
Expand Down Expand Up @@ -449,28 +469,42 @@ export class RipgrepSearchInWorkspaceServer implements SearchInWorkspaceServer {

/**
* Attempts to resolve valid file paths from a given list of patterns.
* The given (e.g. workspace) root paths are used to try resolving relative path patterns to an absolute path.
* The given search paths are used to try resolving relative path patterns to an absolute path.
* The resulting map will include all patterns associated to its equivalent file paths.
* The given patterns that are not successfully mapped to a file system path are not included.
* The given patterns that are not successfully mapped to paths are not included.
*/
protected resolvePatternToPathsMap(patterns: string[], rootPaths: string[], keepSuffix: boolean = false): Map<string, string[]> {
protected resolvePatternToPathsMap(patterns: string[], searchPaths: string[]): Map<string, string[]> {
const patternToPathMap = new Map<string, string[]>();

patterns.forEach(pattern => {
rootPaths.forEach(root => {
const foundPath = findPathInSuffixedPattern(root, pattern, keepSuffix);
searchPaths.forEach(root => {
const foundPath = resolveIncludeFolderFromGlob(root, pattern);

if (foundPath) {
let pathArray = patternToPathMap.get(pattern);
pathArray = pathArray ? [...pathArray, foundPath] : [foundPath];
patternToPathMap.set(pattern, pathArray);
const pathArray = patternToPathMap.get(pattern);
patternToPathMap.set(pattern, pushIfNotIncluded(pathArray, foundPath));
}
});
});

return patternToPathMap;
}

/**
* Transforms relative patterns to absolute paths, one for each given search path.
*/
protected replaceRelativePatternsToAbsolute(patterns: string[], searchPaths: string[]): string[] {
const processedArray: string[] = [];

patterns.forEach(pattern => {
searchPaths.forEach(root => {
pushIfNotIncluded(processedArray, relativeToAbsolutePattern(root, pattern));
});
});

return processedArray;
}

/**
* Returns the root folder uri that a file belongs to.
* In case that a file belongs to more than one root folders, returns the root folder that is closest to the file.
Expand Down

0 comments on commit 1adc8c1

Please sign in to comment.