diff --git a/integration-tests/config/cspell.json b/integration-tests/config/cspell.json new file mode 100644 index 000000000000..0083cf652cab --- /dev/null +++ b/integration-tests/config/cspell.json @@ -0,0 +1,4 @@ +{ + "version": "0.2", + "import": ["../repositories/cspell.yaml"] +} diff --git a/integration-tests/config/repositories/php/php-src/cspell.json b/integration-tests/config/repositories/php/php-src/cspell.json index 04e41079343f..02664b72834d 100644 --- a/integration-tests/config/repositories/php/php-src/cspell.json +++ b/integration-tests/config/repositories/php/php-src/cspell.json @@ -31,5 +31,5 @@ "languageId": "c,cpp,h" } ], - "import": ["../../../../repositories/cspell-reporter.json"] + "import": ["../../../cspell.json"] } diff --git a/integration-tests/snapshots/php/php-src/report.yaml b/integration-tests/snapshots/php/php-src/report.yaml index 6c12c80461ab..524d65ab1925 100644 --- a/integration-tests/snapshots/php/php-src/report.yaml +++ b/integration-tests/snapshots/php/php-src/report.yaml @@ -3,7 +3,7 @@ Repository: php/php-src Url: https://github.com/php/php-src.git Args: ["--config=${repoConfig}/cspell.json","**/*.{md,c,h,php}"] Summary: - files: 1827 + files: 1823 filesWithIssues: 1106 issues: 19948 errors: 0 diff --git a/integration-tests/snapshots/php/php-src/snapshot.txt b/integration-tests/snapshots/php/php-src/snapshot.txt index 6fbb98e89cae..6eb7de7c35f1 100644 --- a/integration-tests/snapshots/php/php-src/snapshot.txt +++ b/integration-tests/snapshots/php/php-src/snapshot.txt @@ -3,7 +3,7 @@ Repository: php/php-src Url: "https://github.com/php/php-src.git" Args: ["--config=../../../../config/repositories/php/php-src/cspell.json","**/*.{md,c,h,php}"] Lines: - CSpell: Files checked: 1827, Issues found: 19948 in 1106 files + CSpell: Files checked: 1823, Issues found: 19948 in 1106 files exit code: 1 ./CODING_STANDARDS.md:105:8 - Unknown word (setclientencoding) -- pg_setclientencoding ./CODING_STANDARDS.md:126:5 - Unknown word (fooselect) -- fooselect_bar diff --git a/integration-tests/src/check.ts b/integration-tests/src/check.ts index 6688c6b2f245..c9f01bf4b66a 100644 --- a/integration-tests/src/check.ts +++ b/integration-tests/src/check.ts @@ -12,7 +12,7 @@ import { PrefixLogger } from './PrefixLogger'; import { Logger } from './types'; const config = readConfig(); -const cspellArgs = '-u --no-progress --relative --show-context'; +const cspellArgs = '-u --no-progress --relative --show-context --gitignore --gitignore-root=. '; const jsCspell = JSON.stringify(Path.resolve(__dirname, '..', '..', 'bin.js')); const cspellCommand = `node ${jsCspell} ${cspellArgs}`; diff --git a/packages/cspell-gitignore/README.md b/packages/cspell-gitignore/README.md index f8b1b9365bee..42d3c7f60d3b 100644 --- a/packages/cspell-gitignore/README.md +++ b/packages/cspell-gitignore/README.md @@ -11,11 +11,13 @@ npm install -S cspell-gitignore ## Usage ```ts -import { GitIgnore } from 'cspell-gitignore'; +import { GitIgnore, findRepoRoot } from 'cspell-gitignore'; // ... -const gitIgnore = new GitIgnore(); +const cwd = process.cwd(); +const root = (await findRepoRoot(cwd)) || cwd; +const gitIgnore = new GitIgnore([root]); const allFiles = glob('**'); @@ -34,3 +36,35 @@ To prevent searching higher in the directory hierarchy, specify roots: ```ts const gitIgnore = new GitIgnore([process.cwd()]); ``` + +# `cspell-gitignore` CLI + +`cspell-gitignore` provides a simple cli for debugging .gitignore issues. + +In most cases it should provide the same output as `git check-ignore`. + +## Usage + +```text +Usage cspell-gitignore [options] + +Check files against .gitignore +Compare against git check-ignore -v -n + +Options: + -r, --root Add a root to prevent searching for .gitignore files above the root if the file is under the root. + This option can be used multiple times to add multiple roots. The default root is the current + repository root determined by the `.git` directory. + +Example: + cspell-gitignore README.md + cspell-gitignore -r . node_modules + +``` + +## Example: + +```sh +$ cspell-gitignore -r . node_modules +.gitignore:58:node_modules/ node_modules +``` diff --git a/packages/cspell-gitignore/bin.js b/packages/cspell-gitignore/bin.js new file mode 100755 index 000000000000..302af0b0dd31 --- /dev/null +++ b/packages/cspell-gitignore/bin.js @@ -0,0 +1,6 @@ +#!/usr/bin/env node +'use strict'; + +const app = require('./dist/app.js'); + +app.run(process.argv); diff --git a/packages/cspell-gitignore/package.json b/packages/cspell-gitignore/package.json index 696531da7032..2a05edff2bf7 100644 --- a/packages/cspell-gitignore/package.json +++ b/packages/cspell-gitignore/package.json @@ -11,11 +11,15 @@ "homepage": "https://github.com/streetsidesoftware/cspell/tree/master/packages/cspell-gitignore#readme", "license": "MIT", "main": "dist/index.js", + "bin": { + "cspell-gitignore": "bin.js" + }, "directories": { "dist": "dist" }, "typings": "dist/index.d.ts", "files": [ + "bin.js", "dist", "!**/__mocks__", "!**/*.test.*", diff --git a/packages/cspell-gitignore/src/GitIgnore.test.ts b/packages/cspell-gitignore/src/GitIgnore.test.ts index e6f198f43770..6386944f6f1c 100644 --- a/packages/cspell-gitignore/src/GitIgnore.test.ts +++ b/packages/cspell-gitignore/src/GitIgnore.test.ts @@ -6,6 +6,9 @@ const packages = path.resolve(pkg, '..'); const gitRoot = path.resolve(packages, '..'); const samples = path.resolve(pkg, 'samples'); const pkgCSpellLib = path.join(packages, 'cspell-lib'); +const gitIgnoreFile = path.resolve(gitRoot, '.gitignore'); +// const pathSamples = path.resolve(pkg, 'samples'); +// const gitIgnoreSamples = path.resolve(pathSamples, '.gitignore'); describe('GitIgnoreServer', () => { test('GitIgnoreServer', () => { @@ -29,18 +32,40 @@ describe('GitIgnoreServer', () => { // cspell:ignore keepme test.each` - file | expected - ${__filename} | ${false} - ${p(samples, 'ignored/keepme.md')} | ${false} - ${p(samples, 'ignored/file.txt')} | ${true} - ${p(pkg, 'node_modules/bin')} | ${true} - `('isIgnored $file', async ({ file, expected }) => { + file | roots | expected + ${__filename} | ${undefined} | ${false} + ${p(samples, 'ignored/keepme.md')} | ${undefined} | ${false} + ${p(samples, 'ignored/file.txt')} | ${undefined} | ${true} + ${p(pkg, 'node_modules/bin')} | ${undefined} | ${true} + ${__filename} | ${[p(samples, 'ignored')]} | ${false} + ${p(samples, 'ignored/keepme.md')} | ${[p(samples, 'ignored')]} | ${false} + ${p(samples, 'ignored/file.txt')} | ${[p(samples, 'ignored')]} | ${false} + ${p(pkg, 'node_modules/bin')} | ${[p(samples, 'ignored')]} | ${true} + `('isIgnored $file $roots', async ({ file, roots, expected }) => { const dir = path.dirname(file); - const gs = new GitIgnore(); + const gs = new GitIgnore(roots); const r = await gs.findGitIgnoreHierarchy(dir); expect(r.isIgnored(file)).toEqual(expected); }); + // cspell:ignore keepme + test.each` + file | roots | expected + ${__filename} | ${undefined} | ${undefined} + ${p(samples, 'ignored/keepme.md')} | ${undefined} | ${undefined} + ${p(samples, 'ignored/file.txt')} | ${undefined} | ${{ glob: 'ignored/**', matched: true, line: 3, root: samples, gitIgnoreFile: p(samples, '.gitignore') }} + ${p(pkg, 'node_modules/bin')} | ${undefined} | ${{ glob: 'node_modules/', matched: true, line: 58, root: gitRoot, gitIgnoreFile: gitIgnoreFile }} + ${__filename} | ${[p(samples, 'ignored')]} | ${undefined} + ${p(samples, 'ignored/keepme.md')} | ${[p(samples, 'ignored')]} | ${undefined} + ${p(samples, 'ignored/file.txt')} | ${[p(samples, 'ignored')]} | ${undefined} + ${p(pkg, 'node_modules/bin')} | ${[p(samples, 'ignored')]} | ${{ glob: 'node_modules/', matched: true, line: 58, root: gitRoot, gitIgnoreFile: gitIgnoreFile }} + `('isIgnoredEx $file $roots', async ({ file, roots, expected }) => { + const dir = path.dirname(file); + const gs = new GitIgnore(roots); + const r = await gs.findGitIgnoreHierarchy(dir); + expect(r.isIgnoredEx(file)).toEqual(expected); + }); + test('isIgnored files', async () => { const files = [ __filename, @@ -53,6 +78,38 @@ describe('GitIgnoreServer', () => { expect(r).toEqual([__filename, p(samples, 'ignored/keepme.md')]); }); + test.each` + file | roots | addRoots | expectedBefore | expectedAfter + ${__filename} | ${undefined} | ${[p(samples, 'ignored')]} | ${false} | ${false} + ${p(samples, 'ignored/keepme.md')} | ${undefined} | ${[p(samples, 'ignored')]} | ${false} | ${false} + ${p(samples, 'ignored/file.txt')} | ${undefined} | ${[p(samples, 'ignored')]} | ${true} | ${false} + ${p(pkg, 'node_modules/bin')} | ${undefined} | ${[p(samples, 'ignored')]} | ${true} | ${true} + `('addRoots $file $addRoots', async ({ file, roots, addRoots, expectedBefore, expectedAfter }) => { + const gs = new GitIgnore(roots); + const before = await gs.isIgnored(file); + expect(before).toEqual(expectedBefore); + gs.addRoots(addRoots); + const after = await gs.isIgnored(file); + expect(after).toEqual(expectedAfter); + }); + + test('addRoots only reset cache if a new root is added', async () => { + const dir = p(samples, 'ignored'); + const gs = new GitIgnore(); + gs.findGitIgnoreHierarchy(dir); + const p0 = gs.peekGitIgnoreHierarchy(dir); + expect(p0).toBeDefined(); + gs.addRoots([dir]); + expect(gs.peekGitIgnoreHierarchy(dir)).toBeUndefined(); + gs.findGitIgnoreHierarchy(dir); + const p1 = gs.peekGitIgnoreHierarchy(dir); + expect(p1).not.toBe(p0); + gs.addRoots([dir]); + expect(gs.peekGitIgnoreHierarchy(dir)).toBe(p1); + gs.findGitIgnoreHierarchy(dir); + expect(gs.peekGitIgnoreHierarchy(dir)).toBe(p1); + }); + function p(dir: string, ...dirs: string[]) { return path.join(dir, ...dirs); } diff --git a/packages/cspell-gitignore/src/GitIgnore.ts b/packages/cspell-gitignore/src/GitIgnore.ts index 4a093ad4bde4..769436f27371 100644 --- a/packages/cspell-gitignore/src/GitIgnore.ts +++ b/packages/cspell-gitignore/src/GitIgnore.ts @@ -1,6 +1,6 @@ import * as path from 'path'; import { contains } from '.'; -import { GitIgnoreHierarchy, loadGitIgnore } from './GitIgnoreFile'; +import { GitIgnoreHierarchy, IsIgnoredExResult, loadGitIgnore } from './GitIgnoreFile'; /** * Class to cache and process `.gitignore` file queries. @@ -8,7 +8,8 @@ import { GitIgnoreHierarchy, loadGitIgnore } from './GitIgnoreFile'; export class GitIgnore { private resolvedGitIgnoreHierarchies = new Map(); private knownGitIgnoreHierarchies = new Map>(); - readonly roots: string[]; + private _roots: Set; + private _sortedRoots: string[]; /** * @param roots - (search roots) an optional array of root paths to prevent searching for `.gitignore` files above the root. @@ -16,9 +17,8 @@ export class GitIgnore { * the search for `.gitignore` will go all the way to the system root of the file. */ constructor(roots: string[] = []) { - this.roots = roots.map((a) => path.resolve(a)); - this.roots.sort((a, b) => a.length - b.length); - Object.freeze(this.roots); + this._sortedRoots = resolveAndSortRoots(roots); + this._roots = new Set(this._sortedRoots); } findResolvedGitIgnoreHierarchy(directory: string): GitIgnoreHierarchy | undefined { @@ -35,6 +35,11 @@ export class GitIgnore { return gh.isIgnored(file); } + async isIgnoredEx(file: string): Promise { + const gh = await this.findGitIgnoreHierarchy(path.dirname(file)); + return gh.isIgnoredEx(file); + } + async findGitIgnoreHierarchy(directory: string): Promise { const known = this.knownGitIgnoreHierarchies.get(directory); if (known) { @@ -60,6 +65,28 @@ export class GitIgnore { return result; } + get roots(): string[] { + return this._sortedRoots; + } + + addRoots(roots: string[]): void { + const rootsToAdd = roots.map((p) => path.resolve(p)).filter((r) => !this._roots.has(r)); + if (!rootsToAdd.length) return; + + rootsToAdd.forEach((r) => this._roots.add(r)); + this._sortedRoots = resolveAndSortRoots([...this._roots]); + this.cleanCachedEntries(); + } + + peekGitIgnoreHierarchy(directory: string): Promise | undefined { + return this.knownGitIgnoreHierarchies.get(directory); + } + + private cleanCachedEntries() { + this.knownGitIgnoreHierarchies.clear(); + this.resolvedGitIgnoreHierarchies.clear(); + } + private async _findGitIgnoreHierarchy(directory: string): Promise { const root = this.determineRoot(directory); const parent = path.dirname(directory); @@ -82,3 +109,19 @@ export class GitIgnore { return path.parse(directory).root; } } + +function resolveAndSortRoots(roots: string[]): string[] { + const sortedRoots = roots.map((a) => path.resolve(a)); + sortRoots(sortedRoots); + Object.freeze(sortedRoots); + return sortedRoots; +} + +/** + * Sorts root paths based upon their length. + * @param roots - array to be sorted + */ +function sortRoots(roots: string[]): string[] { + roots.sort((a, b) => a.length - b.length); + return roots; +} diff --git a/packages/cspell-gitignore/src/GitIgnoreFile.test.ts b/packages/cspell-gitignore/src/GitIgnoreFile.test.ts index 73cd7c0226ee..cb83a327ae4c 100644 --- a/packages/cspell-gitignore/src/GitIgnoreFile.test.ts +++ b/packages/cspell-gitignore/src/GitIgnoreFile.test.ts @@ -4,6 +4,10 @@ import { GitIgnoreFile, GitIgnoreHierarchy, loadGitIgnore, __testing__ } from '. const { mustBeHierarchical } = __testing__; +const pathPackage = path.resolve(__dirname, '..'); +const pathRepo = path.resolve(pathPackage, '../..'); +const gitIgnoreFile = path.resolve(pathRepo, '.gitignore'); + describe('GitIgnoreFile', () => { test('GitIgnoreFile', () => { const gif = sampleGitIgnoreFile(); @@ -52,8 +56,24 @@ describe('GitIgnoreHierarchy', () => { ).toThrowError('Hierarchy violation - files are not nested'); }); - function p(file: string): string { - return path.join(__dirname, file); + test.each` + file | expected + ${__filename} | ${{ matched: true, gitIgnoreFile: p('./.gitignore'), line: undefined, glob: '*.test.*', root: __dirname }} + ${p('GitIgnoreFiles.ts')} | ${undefined} + ${require.resolve('jest')} | ${{ matched: true, gitIgnoreFile, glob: 'node_modules/', line: 58, root: pathRepo }} + ${p('package-lock.json')} | ${undefined} + `('ignoreEx $file', async ({ file, expected }) => { + // cspell:ignore gifs + const gifs = []; + const gi = await loadGitIgnore(path.join(__dirname, '../../..')); + if (gi) gifs.push(gi); + gifs.push(sampleGitIgnoreFile()); + const gih = new GitIgnoreHierarchy(gifs); + expect(gih.isIgnoredEx(file)).toEqual(expected); + }); + + function p(...files: string[]): string { + return path.resolve(__dirname, ...files); } }); @@ -74,3 +94,7 @@ function sampleGitIgnoreFile(): GitIgnoreFile { const file = path.join(m.root, '.gitignore'); return new GitIgnoreFile(m, file); } + +// function oc(v: Partial): T { +// return expect.objectContaining(v); +// } diff --git a/packages/cspell-gitignore/src/GitIgnoreFile.ts b/packages/cspell-gitignore/src/GitIgnoreFile.ts index 7b002c712bad..40de32c8b567 100644 --- a/packages/cspell-gitignore/src/GitIgnoreFile.ts +++ b/packages/cspell-gitignore/src/GitIgnoreFile.ts @@ -1,14 +1,20 @@ -import { GlobMatcher } from 'cspell-glob'; +import { GlobMatcher, GlobMatchRule, GlobPatternNormalized } from 'cspell-glob'; import { promises as fs } from 'fs'; import * as path from 'path'; +export interface IsIgnoredExResult { + glob: string | undefined; + root: string | undefined; + matched: boolean; + gitIgnoreFile: string; + line: number | undefined; +} + /** * Represents an instance of a .gitignore file. */ export class GitIgnoreFile { - constructor(readonly matcher: GlobMatcher, readonly gitignore: string) { - this.gitignore = path.join(matcher.root, '.gitignore'); - } + constructor(readonly matcher: GlobMatcher, readonly gitignore: string) {} get root(): string { return this.matcher.root; @@ -18,10 +24,27 @@ export class GitIgnoreFile { return this.matcher.match(file); } + isIgnoredEx(file: string): IsIgnoredExResult { + const m = this.matcher.matchEx(file); + const { matched } = m; + const partial: Partial = m; + const pattern: Partial | undefined = partial.pattern; + const glob = pattern?.rawGlob ?? partial.glob; + const root = partial.root; + const line = pattern?.line; + return { glob, matched, gitIgnoreFile: this.gitignore, root, line }; + } + static async loadGitignore(gitignore: string): Promise { + gitignore = path.resolve(gitignore); const content = await fs.readFile(gitignore, 'utf8'); const options = { root: path.dirname(gitignore) }; - const globMatcher = new GlobMatcher(content, options); + const globs = content.split('\n').map((glob, index) => ({ + glob, + source: gitignore, + line: index + 1, + })); + const globMatcher = new GlobMatcher(globs, options); return new GitIgnoreFile(globMatcher, gitignore); } } @@ -41,6 +64,20 @@ export class GitIgnoreHierarchy { return false; } + + /** + * Check to see which `.gitignore` file ignored the given file. + * @param file - fsPath to check. + * @returns IsIgnoredExResult of the match or undefined if there was no match. + */ + isIgnoredEx(file: string): IsIgnoredExResult | undefined { + for (const gif of this.gitIgnoreChain) { + const r = gif.isIgnoredEx(file); + if (r.matched) return r; + } + + return undefined; + } } export async function loadGitIgnore(dir: string): Promise { diff --git a/packages/cspell-gitignore/src/__snapshots__/app.test.ts.snap b/packages/cspell-gitignore/src/__snapshots__/app.test.ts.snap new file mode 100644 index 000000000000..18b497c26167 --- /dev/null +++ b/packages/cspell-gitignore/src/__snapshots__/app.test.ts.snap @@ -0,0 +1,54 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app app.help 1`] = ` +Array [ + Array [ + "Usage cspell-gitignore [options] + +Check files against .gitignore +Compare against git check-ignore -v -n + +Options: +-r, --root Add a root to prevent searching for .gitignore files above the root if the file is under the root. + This option can be used multiple times to add multiple roots. The default root is the current + repository root determined by the \`.git\` directory. + +Example: + cspell-gitignore README.md + cspell-gitignore -r . node_modules +", + ], +] +`; + +exports[`app app.run [" "] 1`] = `""`; + +exports[`app app.run [" "] 2`] = `"Missing files"`; + +exports[`app app.run ["../node_modules"] 1`] = `".gitignore:58:node_modules/ ../node_modules"`; + +exports[`app app.run ["../node_modules"] 2`] = `""`; + +exports[`app app.run ["-r", ".", "dist"] 1`] = `"packages/cspell-gitignore/.gitignore:3:dist dist"`; + +exports[`app app.run ["-r", ".", "dist"] 2`] = `""`; + +exports[`app app.run ["-r", ".", "temp"] 1`] = `"packages/cspell-gitignore/.gitignore:4:temp temp"`; + +exports[`app app.run ["-r", ".", "temp"] 2`] = `""`; + +exports[`app app.run ["-r", "."] 1`] = `""`; + +exports[`app app.run ["-r", "."] 2`] = `"Missing files"`; + +exports[`app app.run ["src/code.ts"] 1`] = `":: src/code.ts"`; + +exports[`app app.run ["src/code.ts"] 2`] = `""`; + +exports[`app app.run ["temp"] 1`] = `".gitignore:53:temp temp"`; + +exports[`app app.run ["temp"] 2`] = `""`; + +exports[`app app.run [] 1`] = `""`; + +exports[`app app.run [] 2`] = `"Missing files"`; diff --git a/packages/cspell-gitignore/src/app.test.ts b/packages/cspell-gitignore/src/app.test.ts new file mode 100644 index 000000000000..3a08aa0338c4 --- /dev/null +++ b/packages/cspell-gitignore/src/app.test.ts @@ -0,0 +1,48 @@ +import * as app from './app'; +import * as path from 'path'; + +const log = jest.spyOn(console, 'log').mockImplementation(); +const error = jest.spyOn(console, 'error').mockImplementation(); + +describe('app', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + test.each` + params + ${[path.basename(__dirname) + '/code.ts']} + ${['../node_modules']} + ${['-r', '.', 'dist']} + ${['temp']} + ${['-r', '.', 'temp']} + ${['-r', '.']} + ${[]} + ${[' ']} + `('app.run $params', async ({ params }) => { + await app.run(['', '', ...params]); + const stderr = error.mock.calls + .map((c) => c.join('')) + .join('\n') + .replace(/\\/g, '/'); + const stdout = log.mock.calls + .map((c) => c.join('')) + .join('\n') + .replace(/\\/g, '/'); + expect(stdout).toMatchSnapshot(); + expect(stderr).toMatchSnapshot(); + }); + + test.each` + params | expected + ${['-r']} | ${new Error('Missing root parameter.')} + `('app.run errors $params', async ({ params, expected }) => { + await expect(app.run(['', '', ...params])).rejects.toEqual(expected); + }); + + test('app.help', async () => { + await app.run(['', '', '--help']); + expect(error).toHaveBeenCalledTimes(0); + expect(log.mock.calls).toMatchSnapshot(); + }); +}); diff --git a/packages/cspell-gitignore/src/app.ts b/packages/cspell-gitignore/src/app.ts new file mode 100644 index 000000000000..0b7e915cbaba --- /dev/null +++ b/packages/cspell-gitignore/src/app.ts @@ -0,0 +1,101 @@ +import { GitIgnore, findRepoRoot } from '.'; + +import * as path from 'path'; + +type OptionParser = (params: string[]) => string[]; + +const helpText = `Usage cspell-gitignore [options] + +Check files against .gitignore +Compare against git check-ignore -v -n + +Options: +-r, --root Add a root to prevent searching for .gitignore files above the root if the file is under the root. + This option can be used multiple times to add multiple roots. The default root is the current + repository root determined by the \`.git\` directory. + +Example: + cspell-gitignore README.md + cspell-gitignore -r . node_modules +`; + +export async function run(args: string[]): Promise { + const { roots, files, help } = parseArgs(args.slice(2)); + const cwd = process.cwd(); + const repo = (await findRepoRoot(cwd)) || cwd; + const gi = await createGitIgnore(roots, repo); + + if (help) { + console.log(help); + return; + } + + if (!files.length) { + console.error('Missing files'); + process.exitCode = 1; + return; + } + + for (const file of files) { + const filename = path.relative(cwd, file); + const pFile = gi.isIgnoredEx(file); + const pDir = gi.isIgnoredEx(file + '/ '); + const r = (await pFile) || (await pDir); + const gitignore = r?.gitIgnoreFile ? path.relative(repo, r.gitIgnoreFile) : ''; + const line = r?.line || ''; + const glob = r?.glob || ''; + console.log(`${gitignore}:${line}:${glob}\t${filename}`); + } +} + +function parseArgs(params: string[]) { + const roots: string[] = []; + const files: string[] = []; + let help = ''; + + const options: Record = { + '-r': optionRoot, + '--root': optionRoot, + '-h': optionHelp, + '--help': optionHelp, + }; + + function optionRoot(params: string[]): string[] { + const root = params[1]; + if (!root) { + throw new Error('Missing root parameter.'); + } + roots.push(path.resolve(root)); + return params.slice(2); + } + + function optionFile(params: string[]): string[] { + const file = params[0].trim(); + + if (file) { + files.push(path.resolve(file)); + } + + return params.slice(1); + } + + function optionHelp(_params: string[]) { + help = helpText; + return []; + } + + while (params.length) { + const fn = options[params[0]]; + params = fn?.(params) ?? optionFile(params); + } + + return { roots, files, help }; +} + +async function createGitIgnore(roots: string[], repoRoot: string) { + if (!roots.length) { + roots.push(repoRoot); + } + + return new GitIgnore(roots); +} diff --git a/packages/cspell-gitignore/src/helpers.test.ts b/packages/cspell-gitignore/src/helpers.test.ts index 18f429737d76..44888b303275 100644 --- a/packages/cspell-gitignore/src/helpers.test.ts +++ b/packages/cspell-gitignore/src/helpers.test.ts @@ -14,9 +14,13 @@ describe('helpers', () => { expect(path.resolve(r, p)).toEqual(dir); }); - test('findRepoRoot', async () => { - const f = await findRepoRoot(__dirname); - expect(f).toEqual(path.join(__dirname, '../../..')); + test.each` + dir | expected + ${__dirname} | ${path.join(__dirname, '../../..')} + ${'/'} | ${undefined} + `('findRepoRoot $dir', async ({ dir, expected }) => { + const f = await findRepoRoot(dir); + expect(f).toEqual(expected); }); test.each` diff --git a/packages/cspell-glob/src/GlobMatcherTypes.ts b/packages/cspell-glob/src/GlobMatcherTypes.ts index 5fd3ee5f5a61..9c9a7c0a3f38 100644 --- a/packages/cspell-glob/src/GlobMatcherTypes.ts +++ b/packages/cspell-glob/src/GlobMatcherTypes.ts @@ -42,6 +42,10 @@ export interface GlobPatternWithOptionalRoot { * Optional value useful for tracing which file a glob pattern was defined in. */ source?: string; + /** + * Optional line number in the source + */ + line?: number; } export interface GlobPatternWithRoot extends GlobPatternWithOptionalRoot { diff --git a/packages/cspell/bin.js b/packages/cspell/bin.js index 7625ebe49f54..b5936e661284 100755 --- a/packages/cspell/bin.js +++ b/packages/cspell/bin.js @@ -1,5 +1,4 @@ #!/usr/bin/env node - 'use strict'; const app = require('./dist/app'); diff --git a/packages/cspell/src/lint.ts b/packages/cspell/src/lint.ts index de0c1795bb99..61353cf48de8 100644 --- a/packages/cspell/src/lint.ts +++ b/packages/cspell/src/lint.ts @@ -16,7 +16,7 @@ import { buildGlobMatcher, extractGlobsFromMatcher, extractPatterns, normalizeGl import { loadReporters, mergeReporters } from './util/reporters'; import { getTimeMeasurer } from './util/timer'; import * as util from './util/util'; -import { GitIgnore } from 'cspell-gitignore'; +import { findRepoRoot, GitIgnore } from 'cspell-gitignore'; export async function runLint(cfg: CSpellApplicationConfiguration): Promise { let { reporter } = cfg; @@ -185,7 +185,7 @@ export async function runLint(cfg: CSpellApplicationConfiguration): Promise !!r) : roots) || []; + if (!root?.length) { + const cwd = process.cwd(); + const repo = (await findRepoRoot(cwd)) || cwd; + root.push(repo); + } return new GitIgnore(root?.map((p) => path.resolve(p))); }