Skip to content

Commit

Permalink
fix: handle symbolic links to config files (#5122)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jason3S authored Dec 28, 2023
1 parent f27c262 commit d9acaaf
Show file tree
Hide file tree
Showing 13 changed files with 146 additions and 11 deletions.
1 change: 1 addition & 0 deletions packages/cspell-io/samples/link.txt
14 changes: 14 additions & 0 deletions packages/cspell-io/src/VirtualFS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -515,12 +515,14 @@ export interface VfsStat extends Stats {
isDirectory(): boolean;
isFile(): boolean;
isUnknown(): boolean;
isSymbolicLink(): boolean;
}

export interface VfsDirEntry extends DirEntry {
isDirectory(): boolean;
isFile(): boolean;
isUnknown(): boolean;
isSymbolicLink(): boolean;
}

class CFileType {
Expand All @@ -537,6 +539,10 @@ class CFileType {
isUnknown(): boolean {
return !this.fileType;
}

isSymbolicLink(): boolean {
return !!(this.fileType & FileType.SymbolicLink);
}
}

class CVfsStat extends CFileType implements VfsStat {
Expand Down Expand Up @@ -576,6 +582,14 @@ class CVfsDirEntry extends CFileType implements VfsDirEntry {
this._url = new URL(this.entry.name, this.entry.dir);
return this._url;
}

toJSON(): DirEntry {
return {
name: this.name,
dir: this.dir,
fileType: this.fileType,
};
}
}

function chopUrl(url: URL | undefined): string {
Expand Down
30 changes: 28 additions & 2 deletions packages/cspell-io/src/VirtualFs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { toFileURL, urlBasename } from './node/file/url.js';
import { pathToSample as ps } from './test/test.helper.js';
import type { VFileSystemProvider, VirtualFS, VProviderFileSystem } from './VirtualFS.js';
import { createVirtualFS, FSCapabilityFlags, getDefaultVirtualFs, VFSErrorUnsupportedRequest } from './VirtualFS.js';
import { FileType } from './models/Stats.js';

const sc = expect.stringContaining;
const oc = expect.objectContaining;
Expand Down Expand Up @@ -167,6 +168,7 @@ describe('VirtualFs', () => {
filename | baseFilename | content
${__filename} | ${basename(__filename)} | ${sc('This bit of text')}
${ps('cities.txt')} | ${'cities.txt'} | ${sc('San Francisco\n')}
${ps('link.txt')} | ${'link.txt'} | ${sc('San Francisco\n')}
${ps('cities.txt.gz')} | ${'cities.txt.gz'} | ${sc('San Francisco\n')}
`('readFile $filename', async ({ filename, content, baseFilename }) => {
const url = toFileURL(filename);
Expand All @@ -192,8 +194,9 @@ describe('VirtualFs', () => {
});

test.each`
url | expected
${__filename} | ${oc({ mtimeMs: expect.any(Number), size: expect.any(Number), fileType: 1 })}
url | expected
${__filename} | ${oc({ mtimeMs: expect.any(Number), size: expect.any(Number), fileType: 1 })}
${ps('link.txt')} | ${oc({ mtimeMs: expect.any(Number), size: expect.any(Number), fileType: 1 }) /* links are resolved */}
`('getStat $url', async ({ url, expected }) => {
url = toFileURL(url);
const fs = getDefaultVirtualFs().fs;
Expand Down Expand Up @@ -262,6 +265,29 @@ describe('VirtualFs', () => {
expect(capabilities.readDirectory).toBe(true);
expect(capabilities.writeDirectory).toBe(false);
});

test('symbolic links', async () => {
const linkPath = ps('link.txt');
const linkURL = toFileURL(linkPath);
const fs = virtualFs.fs;

const stat = await fs.stat(linkURL);
expect(stat.isFile()).toBe(true);
expect(stat.isDirectory()).toBe(false);
expect(stat.isSymbolicLink()).toBe(false);

const result = fs.readDirectory(new URL('.', linkURL));
await expect(result).resolves.toEqual(
expect.arrayContaining([
oc({
fileType: FileType.SymbolicLink,
url: linkURL,
name: urlBasename(linkURL),
dir: new URL('.', linkURL),
}),
]),
);
});
});

function mockFileSystem(): VProviderFileSystem {
Expand Down
5 changes: 3 additions & 2 deletions packages/cspell-io/src/handlers/node/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ function direntToDirEntry(dir: URL, dirent: Dirent): DirEntry {
};
}

function toFileType(statLike: { isFile(): boolean; isDirectory(): boolean }): FileType {
return statLike.isFile() ? FileType.File : statLike.isDirectory() ? FileType.Directory : FileType.Unknown;
function toFileType(statLike: { isFile(): boolean; isDirectory(): boolean; isSymbolicLink(): boolean }): FileType {
const t = statLike.isFile() ? FileType.File : statLike.isDirectory() ? FileType.Directory : FileType.Unknown;
return statLike.isSymbolicLink() ? t | FileType.SymbolicLink : t;
}
4 changes: 4 additions & 0 deletions packages/cspell-io/src/models/Stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export enum FileType {
* A directory.
*/
Directory = 2,
/**
* A symbolic link.
*/
SymbolicLink = 64,
}

export interface DirEntry {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { createRedirectProvider, createVirtualFS, FSCapabilityFlags } from 'cspell-io';
import { describe, expect, test } from 'vitest';
import { promises as nfs } from 'node:fs';

import { pathPackageSamplesURL } from '../../../../test-util/index.mjs';
import { pathPackageSamplesURL, pathRepoTestFixturesURL } from '../../../../test-util/index.mjs';
import { getVirtualFS } from '../../../fileSystem.js';
import { resolveFileWithURL, toURL } from '../../../util/url.js';
import { defaultConfigFilenames as searchPlaces } from './configLocations.js';
import { ConfigSearch } from './configSearch.js';

const virtualURL = new URL('virtual-fs://github/cspell-io/');
const htmlURL = new URL('https://example.com/');
const issuesURL = new URL('issues/', pathRepoTestFixturesURL);

describe('ConfigSearch', () => {
describe('searchForConfig', () => {
Expand Down Expand Up @@ -48,6 +50,7 @@ describe('ConfigSearch', () => {
${'https://example.com/path/to/search/from/'} | ${u('.cspell.json', htmlURL)}
${'https://example.com/path/to/files/'} | ${u('.cspell.json', htmlURL)}
${sURLh('package-json/nested/README.md')} | ${sURL('package-json/package.json')}
${uh('issue-5120/', issuesURL)} | ${u('issue-5120/.config/cspell.config.yaml', issuesURL)}
`('searchForConfig vfs $dir', async ({ dir, expected }) => {
const vfs = createVirtualFS();
const redirectProviderVirtual = createRedirectProvider('virtual', virtualURL, sURL('./'));
Expand Down Expand Up @@ -82,6 +85,13 @@ describe('ConfigSearch', () => {
expect(result4).toBe(result3);
expect(result5).toStrictEqual(result4);
});

test('symbolic links', async () => {
const dir = new URL('issues/issue-5120/', pathRepoTestFixturesURL);
const info = await nfs.readdir(dir, { withFileTypes: true });
const symEntries = info.filter((e) => e.isSymbolicLink());
expect(symEntries.map((e) => e.name)).toEqual(['.config']);
});
});

describe('clearCache', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,22 +89,28 @@ export class ConfigSearch {
const parentInfo = await parentInfoP;
const name = urlBasename(dir).slice(0, -1);
const found = parentInfo.get(name);
if (!found?.isDirectory()) return false;
if (!found?.isDirectory() && !found?.isSymbolicLink()) return false;
}
const dirUrlHref = dir.href;
const dirInfo = await dirInfoCache.get(
dirUrlHref,
async () => new Map((await this.fs.readDirectory(dir).catch(() => [])).map((ent) => [ent.name, ent])),
);
const dirInfo = await dirInfoCache.get(dirUrlHref, async () => await this.readDir(dir));

const name = urlBasename(filename);
const found = dirInfo.get(name);
return !!found?.isFile();
return found?.isFile() || found?.isSymbolicLink() || false;
};

return hasFile;
}

private async readDir(dir: URL): Promise<Map<string, VfsDirEntry>> {
try {
const dirInfo = await this.fs.readDirectory(dir);
return new Map(dirInfo.map((ent) => [ent.name, ent]));
} catch (e) {
return new Map();
}
}

private createHasFileStatCheck(): (file: URL) => Promise<boolean> {
const hasFile = async (filename: URL): Promise<boolean> => {
const stat = await this.fs.stat(filename).catch(() => undefined);
Expand Down
1 change: 1 addition & 0 deletions test-fixtures/issues/issue-5120/.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
words:
- sampleword
- somenewword
- Sonoma
64 changes: 64 additions & 0 deletions test-fixtures/issues/issue-5120/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
### Kind of Issue

Runtime - command-line tools

### Tool or Library

cspell

### Version

8.2.3

### Supporting Library

Not sure

### OS

Macos

### OS Version

Sonoma 14.2.1

### Description

cspell does not automatically read a configuration file if it is a symlink.

### Steps to Reproduce

1. Define `somenewword` in a simple config file: `cspell.yaml`
2. Confirm it works: `cspell trace somenewword`
3. Rename it: `mv cspell.yaml actual.yaml`
4. Create a symlink to it: `ln -s actual.yaml cspell.yaml`
5. Try using it implicitly: `cspell trace somenewword`. **The config is not loaded.**
6. Use it explicitly: `cspell trace somenewword --config cspell.yaml`. The config is loaded.

### Expected Behavior

cspell finds and reads `cspell.yaml` no matter whether it is a symlink or an actual file.

### Additional Information

_No response_

### cspell.json

_No response_

### cspell.config.yaml

```yml
words:
- somenewword
```
### Example Repository
_No response_
### Code of Conduct
- [X] I agree to follow this project's Code of Conduct
1 change: 1 addition & 0 deletions test-fixtures/issues/issue-5120/docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Nested example.
2 changes: 2 additions & 0 deletions test-fixtures/issues/issue-5120/docs/cspell.actual.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import:
- ../.config/cspell-imports.yaml
1 change: 1 addition & 0 deletions test-fixtures/issues/issue-5120/docs/cspell.config.yaml

0 comments on commit d9acaaf

Please sign in to comment.