diff --git a/lib/__tests__/fixtures/globs/[digit]/not-digits/styles.css b/lib/__tests__/fixtures/globs/[digit]/not-digits/styles.css new file mode 100644 index 0000000000..077f6dd7c0 --- /dev/null +++ b/lib/__tests__/fixtures/globs/[digit]/not-digits/styles.css @@ -0,0 +1 @@ +a {} diff --git a/lib/__tests__/fixtures/globs/extglob!(s)/styles.css b/lib/__tests__/fixtures/globs/extglob!(s)/styles.css new file mode 100644 index 0000000000..077f6dd7c0 --- /dev/null +++ b/lib/__tests__/fixtures/globs/extglob!(s)/styles.css @@ -0,0 +1 @@ +a {} diff --git a/lib/__tests__/fixtures/globs/glob+chars/glob-plus.css b/lib/__tests__/fixtures/globs/glob+chars/glob-plus.css new file mode 100644 index 0000000000..077f6dd7c0 --- /dev/null +++ b/lib/__tests__/fixtures/globs/glob+chars/glob-plus.css @@ -0,0 +1 @@ +a {} diff --git a/lib/__tests__/fixtures/globs/glob-contains-plus/styles.css b/lib/__tests__/fixtures/globs/glob-contains-plus/styles.css new file mode 100644 index 0000000000..077f6dd7c0 --- /dev/null +++ b/lib/__tests__/fixtures/globs/glob-contains-plus/styles.css @@ -0,0 +1 @@ +a {} diff --git a/lib/__tests__/fixtures/globs/got!negate/negate/styles.css b/lib/__tests__/fixtures/globs/got!negate/negate/styles.css new file mode 100644 index 0000000000..077f6dd7c0 --- /dev/null +++ b/lib/__tests__/fixtures/globs/got!negate/negate/styles.css @@ -0,0 +1 @@ +a {} diff --git a/lib/__tests__/fixtures/globs/got[braces] and (spaces)/styles.css b/lib/__tests__/fixtures/globs/got[braces] and (spaces)/styles.css new file mode 100644 index 0000000000..077f6dd7c0 --- /dev/null +++ b/lib/__tests__/fixtures/globs/got[braces] and (spaces)/styles.css @@ -0,0 +1 @@ +a {} diff --git a/lib/__tests__/fixtures/globs/negated-globs/ignore/ignore-this-file.css b/lib/__tests__/fixtures/globs/negated-globs/ignore/ignore-this-file.css new file mode 100644 index 0000000000..077f6dd7c0 --- /dev/null +++ b/lib/__tests__/fixtures/globs/negated-globs/ignore/ignore-this-file.css @@ -0,0 +1 @@ +a {} diff --git a/lib/__tests__/fixtures/globs/negated-globs/lint-this-file.css b/lib/__tests__/fixtures/globs/negated-globs/lint-this-file.css new file mode 100644 index 0000000000..077f6dd7c0 --- /dev/null +++ b/lib/__tests__/fixtures/globs/negated-globs/lint-this-file.css @@ -0,0 +1 @@ +a {} diff --git a/lib/__tests__/fixtures/globs/with spaces/styles.css b/lib/__tests__/fixtures/globs/with spaces/styles.css new file mode 100644 index 0000000000..077f6dd7c0 --- /dev/null +++ b/lib/__tests__/fixtures/globs/with spaces/styles.css @@ -0,0 +1 @@ +a {} diff --git a/lib/__tests__/standalone-globs.test.js b/lib/__tests__/standalone-globs.test.js new file mode 100644 index 0000000000..2f3f486287 --- /dev/null +++ b/lib/__tests__/standalone-globs.test.js @@ -0,0 +1,193 @@ +'use strict'; + +/* eslint-disable node/no-extraneous-require */ + +const describe = require('@jest/globals').describe; +const expect = require('@jest/globals').expect; +const it = require('@jest/globals').it; + +/* eslint-enable */ + +const path = require('path'); +const replaceBackslashes = require('../testUtils/replaceBackslashes'); +const standalone = require('../standalone'); + +const fixturesPath = replaceBackslashes(path.join(__dirname, 'fixtures', 'globs')); + +// Tests for https://github.com/stylelint/stylelint/issues/4521 + +describe('standalone globbing', () => { + describe('paths with special characters', () => { + // ref https://github.com/micromatch/micromatch#matching-features + const fixtureDirs = [ + `[digit]/not-digits`, + `with spaces`, + `extglob!(s)`, + `got!negate/negate`, + // `extglob+(s)`, // Note: +'s cause errors. Ignoring until it becomes a problem + ]; + + // https://github.com/stylelint/stylelint/issues/4193 + it.each(fixtureDirs)(`static path contains "%s"`, async (fixtureDir) => { + const cssPath = `${fixturesPath}/${fixtureDir}/styles.css`; + + const { results } = await standalone({ + files: cssPath, + config: { rules: { 'block-no-empty': true } }, + }); + + expect(results).toHaveLength(1); + expect(results[0].errored).toEqual(true); + expect(results[0].warnings[0]).toEqual( + expect.objectContaining({ + rule: 'block-no-empty', + severity: 'error', + }), + ); + }); + }); + + // https://github.com/stylelint/stylelint/issues/4211 + it('glob has no + character, matched path does', async () => { + const files = `${fixturesPath}/**/glob-plus.css`; // file is in dir 'glob+chars' + + const { results } = await standalone({ + files, + config: { rules: { 'block-no-empty': true } }, + }); + + expect(results).toHaveLength(1); + expect(results[0].errored).toEqual(true); + expect(results[0].warnings[0]).toEqual( + expect.objectContaining({ + rule: 'block-no-empty', + severity: 'error', + }), + ); + }); + + // https://github.com/stylelint/stylelint/issues/4211 + it('glob contains + character, matched path does not', async () => { + const files = `${fixturesPath}/+(g)lob-contains-plus/*.css`; + + const { results } = await standalone({ + files, + config: { rules: { 'block-no-empty': true } }, + }); + + expect(results).toHaveLength(1); + expect(results[0].errored).toEqual(true); + expect(results[0].warnings[0]).toEqual( + expect.objectContaining({ + rule: 'block-no-empty', + severity: 'error', + }), + ); + }); + + // https://github.com/stylelint/stylelint/issues/3272 + // should ignore 'negated-globs/ignore/styles.css' + it('negated glob patterns', async () => { + const files = [ + `${fixturesPath}/negated-globs/**/*.css`, + `!${fixturesPath}/negated-globs/ignore/**/*.css`, + ]; + + const { results } = await standalone({ + files, + config: { rules: { 'block-no-empty': true } }, + }); + + // ensure that the only result is from the unignored file + expect(results[0].source).toEqual(expect.stringContaining('lint-this-file.css')); + + expect(results).toHaveLength(1); + expect(results[0].errored).toEqual(true); + expect(results[0].warnings[0]).toEqual( + expect.objectContaining({ + rule: 'block-no-empty', + severity: 'error', + }), + ); + }); + + describe('mixed globs and paths with special chars', () => { + it('manual escaping', async () => { + const cssGlob = `${fixturesPath}/got\\[braces\\] and \\(spaces\\)/*.+(s|c)ss`; + + const { results } = await standalone({ + files: cssGlob, + config: { + rules: { + 'block-no-empty': true, + }, + }, + }); + + expect(results).toHaveLength(1); + expect(results[0].errored).toEqual(true); + expect(results[0].warnings[0]).toEqual( + expect.objectContaining({ + rule: 'block-no-empty', + severity: 'error', + }), + ); + }); + + it('setting "cwd" in globbyOptions', async () => { + const cssGlob = `*.+(s|c)ss`; + + const { results } = await standalone({ + files: cssGlob, + config: { + rules: { + 'block-no-empty': true, + }, + }, + globbyOptions: { + cwd: `${fixturesPath}/got[braces] and (spaces)/`, + }, + }); + + expect(results).toHaveLength(1); + expect(results[0].errored).toEqual(true); + expect(results[0].warnings[0]).toEqual( + expect.objectContaining({ + rule: 'block-no-empty', + severity: 'error', + }), + ); + }); + + /* eslint-disable jest/no-commented-out-tests -- Failing case for reference. Documents behaviour that doesn't work. */ + + // Note: This fails because there's no way to tell which parts of the glob are literal characters, and which are special globbing characters. + // + // 'got[braces] and (spaces)' is a literal directory path. `*.+(s|c)ss` is a glob. + + // https://github.com/stylelint/stylelint/issues/4855 + // it('glob and matched path contain different special chars, complex example', async () => { + // const cssGlob = `${fixturesPath}/got[braces] and (spaces)/*.+(s|c)ss`; + + // const { results } = await standalone({ + // files: cssGlob, + // config: { + // rules: { + // 'block-no-empty': true, + // }, + // }, + // }); + + // expect(results).toHaveLength(1); + // expect(results[0].errored).toEqual(true); + // expect(results[0].warnings[0]).toEqual( + // expect.objectContaining({ + // rule: 'block-no-empty', + // severity: 'error', + // }), + // ); + // }); + + /* eslint-enable */ + }); +}); diff --git a/lib/standalone.js b/lib/standalone.js index 16d84aa9bb..e92d9c1946 100644 --- a/lib/standalone.js +++ b/lib/standalone.js @@ -4,6 +4,7 @@ const _ = require('lodash'); const createStylelint = require('./createStylelint'); const createStylelintResult = require('./createStylelintResult'); const debug = require('debug')('stylelint:standalone'); +const fastGlob = require('fast-glob'); const FileCache = require('./utils/FileCache'); const filterFilePaths = require('./utils/filterFilePaths'); const formatters = require('./formatters'); @@ -170,6 +171,20 @@ module.exports = function (options) { fileList = [fileList]; } + fileList = fileList.map((entry) => { + if (globby.hasMagic(entry)) { + const cwd = _.get(globbyOptions, 'cwd', process.cwd()); + const absolutePath = !path.isAbsolute(entry) ? path.join(cwd, entry) : path.normalize(entry); + + if (fs.existsSync(absolutePath)) { + // This glob-like path points to a file. Return an escaped path to avoid globbing + return fastGlob.escapePath(entry); + } + } + + return entry; + }); + if (!options.disableDefaultIgnores) { fileList = fileList.concat(ALWAYS_IGNORED_GLOBS.map((glob) => `!${glob}`)); } diff --git a/package.json b/package.json index 8283cc81d2..b91ac5aec3 100644 --- a/package.json +++ b/package.json @@ -118,6 +118,7 @@ "cosmiconfig": "^7.0.0", "debug": "^4.1.1", "execall": "^2.0.0", + "fast-glob": "^3.2.4", "fastest-levenshtein": "^1.0.9", "file-entry-cache": "^5.0.1", "get-stdin": "^8.0.0",