Repository: eslint/eslint (JavaScript)

Issue:
AssertionError [ERR_ASSERTION]: 'newBasePath' should be an absolute path
Windows
Node v12.13.0
npm v6.13.6
eslint 6.8.0
Parser: @typescript-eslint/parser
Bash terminal

eslint config:
```
"use strict";
const allExtensions = [".ts", ".tsx", ".d.ts", ".js", ".jsx"];
module.exports = {
    env: {
        "browser": true,
        "es6": true,
    },
    parser: "@typescript-eslint/parser",
    parserOptions: {
        project: "./tsconfig.json",
        sourceType: "module",
    },
    plugins: [
        "import",
        "@typescript-eslint",
        "@typescript-eslint/tslint",
    ],
    settings: {
        "import/extensions": allExtensions,
        "import/parsers": {
            "@typescript-eslint/parser": [".ts", ".tsx", ".d.ts"],
        },
        "import/resolver": {
            "node": {
                "extensions": allExtensions,
            },
        },
    },
    rules: {
     
        "import/no-deprecated": "warn",
        "@typescript-eslint/no-unused-vars": "error",
        "@typescript-eslint/tslint/config": [
            "warn",
            {
                "rules": {
                    "deprecation": true,
                },
            },
        ],
    },
};
```

I'm currently using a globally installed npm package (call it foo cli) that wraps eslint. I have a workspace in c:\src\myproject. foo cli has an internal reference to eslint 6.8.0. When I run `ap lint` from c:\src\myproject, foo cli invokes eslint with some default parameters, and I get the error from the bug title. Here's the raw output:

```
/c/myproject (master)
$ ap lint
Running C:\Users\drkestel\AppData\Roaming\npm\node_modules\@foocli\node_modules\.bin\eslint.cmd **/*.ts --ignore-path C:\Users\drkestel\AppData\Roaming\npm\node_modules\@foocli\lint\.eslintignore -c C:\Users\drkestel\AppData\Roaming\npm\node_modules\@foocli\lint\.eslintrc.json --resolve-plugins-relative-to C:\Users\drkestel\AppData\Roaming\npm\node_modules\@foocli\lint
AssertionError [ERR_ASSERTION]: 'newBasePath' should be an absolute path.
    at IgnorePattern.getPatternsRelativeTo (C:\Users\drkestel\AppData\Roaming\npm\node_modules\@foocli\node_modules\eslint\lib\cli-engine\config-array\ignore-pattern.js:210:9)
    at C:\Users\drkestel\AppData\Roaming\npm\node_modules\@foocli\node_modules\eslint\lib\cli-engine\config-array\ignore-pattern.js:150:42
    at Array.map (<anonymous>)
    at Function.createIgnore (C:\Users\drkestel\AppData\Roaming\npm\node_modules\@foocli\node_modules\eslint\lib\cli-engine\config-array\ignore-pattern.js:150:31)
    at createConfig (C:\Users\drkestel\AppData\Roaming\npm\node_modules\@foocli\node_modules\eslint\lib\cli-engine\config-array\config-array.js:282:40)
    at ConfigArray.extractConfig (C:\Users\drkestel\AppData\Roaming\npm\node_modules\@foocli\node_modules\eslint\lib\cli-engine\config-array\config-array.js:452:33)
    at FileEnumerator._isIgnoredFile (C:\Users\drkestel\AppData\Roaming\npm\node_modules\@foocli\node_modules\eslint\lib\cli-engine\file-enumerator.js:479:24)
    at FileEnumerator._iterateFilesRecursive (C:\Users\drkestel\AppData\Roaming\npm\node_modules\@foocli\node_modules\eslint\lib\cli-engine\file-enumerator.js:407:38)
    at _iterateFilesRecursive.next (<anonymous>)
    at FileEnumerator.iterateFiles (C:\Users\drkestel\AppData\Roaming\npm\node_modules\@foocli\node_modules\eslint\lib\cli-engine\file-enumerator.js:251:49)
```

I believe the problem exists in ignore-pattern.js and was introduced in this pull request: https://github.com/eslint/eslint/pull/12274/files

If I add some debug logging to IgnorePattern.getCommonAncestorPath:
```
function getCommonAncestorPath(sourcePaths) {
    let result = sourcePaths[0];

    for (let i = 1; i < sourcePaths.length; ++i) {
        const a = result;
        const b = sourcePaths[i];

        // Set the shorter one (it's the common ancestor if one includes the other).
        result = a.length < b.length ? a : b;

        // Set the common ancestor.
        for (let j = 0, lastSepPos = 0; j < a.length && j < b.length; ++j) {
            if (a[j] !== b[j]) {
                result = a.slice(0, lastSepPos);
                break;
            }
            if (a[j] === path.sep) {
                lastSepPos = j;
            }
        }
    }
    console.log("**: ", { result: result, sep: path.sep, sourcePaths: sourcePaths }); // new line here
    return result || path.sep;
}
```

I see the following output:
```
/c/myproject (master)
$ ap lint
Running C:\Users\drkestel\AppData\Roaming\npm\node_modules\@foocli\node_modules\.bin\eslint.cmd **/*.ts --ignore-path C:\Users\drkestel\AppData\Roaming\npm\node_modules\@foocli\lint\.eslintignore -c C:\Users\drkestel\AppData\Roaming\npm\node_modules\@foocli\lint\.eslintrc.json --resolve-plugins-relative-to C:\Users\drkestel\AppData\Roaming\npm\node_modules\@foocli\lint
**:  {
  result: 'C:\\src\\myproject',
  sep: '\\',
  sourcePaths: [ 'C:\\src\\myproject' ]
}
**:  {
  result: 'C:\\src\\myproject',
  sep: '\\',
  sourcePaths: [ 'C:\\src\\myproject' ]
}
**:  {
  result: 'C:',
  sep: '\\',
  sourcePaths: [
    'C:\\src\\myproject',
    'C:\\Users\\drkestel\\AppData\\Roaming\\npm\\node_modules\\@foocli\\lint'
  ]
}
AssertionError [ERR_ASSERTION]: 'newBasePath' should be an absolute path.
    at IgnorePattern.getPatternsRelativeTo (C:\Users\drkestel\AppData\Roaming\npm\node_modules\@foocli\node_modules\eslint\lib\cli-engine\config-array\ignore-pattern.js:210:9)
    at C:\Users\drkestel\AppData\Roaming\npm\node_modules\@foocli\node_modules\eslint\lib\cli-engine\config-array\ignore-pattern.js:150:42
    at Array.map (<anonymous>)
    at Function.createIgnore (C:\Users\drkestel\AppData\Roaming\npm\node_modules\@foocli\node_modules\eslint\lib\cli-engine\config-array\ignore-pattern.js:150:31)
    at createConfig (C:\Users\drkestel\AppData\Roaming\npm\node_modules\@foocli\node_modules\eslint\lib\cli-engine\config-array\config-array.js:282:40)
    at ConfigArray.extractConfig (C:\Users\drkestel\AppData\Roaming\npm\node_modules\@foocli\node_modules\eslint\lib\cli-engine\config-array\config-array.js:452:33)
    at FileEnumerator._isIgnoredFile (C:\Users\drkestel\AppData\Roaming\npm\node_modules\@foocli\node_modules\eslint\lib\cli-engine\file-enumerator.js:479:24)
    at FileEnumerator._iterateFilesRecursive (C:\Users\drkestel\AppData\Roaming\npm\node_modules\@foocli\node_modules\eslint\lib\cli-engine\file-enumerator.js:407:38)
    at _iterateFilesRecursive.next (<anonymous>)
    at FileEnumerator.iterateFiles (C:\Users\drkestel\AppData\Roaming\npm\node_modules\@foocli\node_modules\eslint\lib\cli-engine\file-enumerator.js:251:49)

/c/myproject (master)
$
```

Notice the `result: 'C:',` - that is not considered a valid absolute path.

Interestingly enough, I can mitigate the problem by using windows cmd.exe and setting my current prompt to `c:\` (lowercase, as opposed to `C:\`). Because `c:\ != C:\`, it works. See the output:

```
c:\src\myproject>ap lint
Running C:\Users\drkestel\AppData\Roaming\npm\node_modules\@foocli\node_modules\.bin\eslint.cmd **/*.ts --ignore-path C:\Users\drkestel\AppData\Roaming\npm\node_modules\@foocli\lint\.eslintignore -c C:\Users\drkestel\AppData\Roaming\npm\node_modules\@foocli\lint\.eslintrc.json --resolve-plugins-relative-to C:\Users\drkestel\AppData\Roaming\npm\node_modules\@foocli\lint
**:  {
  result: 'c:\\src\\myproject',
  sep: '\\',
  sourcePaths: [ 'c:\\src\\myproject' ]
}
**:  {
  result: 'c:\\src\\myproject',
  sep: '\\',
  sourcePaths: [ 'c:\\src\\myproject' ]
}
**:  {
  result: '',
  sep: '\\',
  sourcePaths: [
    'c:\\src\\myproject',
    'C:\\Users\\drkestel\\AppData\\Roaming\\npm\\node_modules\\@foocli\\lint'
  ]
}
**:  {
  result: '',
  sep: '\\',
  sourcePaths: [
    'c:\\src\\myproject',
    'C:\\Users\\drkestel\\AppData\\Roaming\\npm\\node_modules\\@foocli\\lint'
  ]
}

c:\src\myproject\Client\Index.ts
  66:5   warning  Missing return type on function                 @typescript-eslint/explicit-function-return-type
  67:17  error    'container' is assigned a value but never used  @typescript-eslint/no-unused-vars
  68:15  error    'id' is assigned a value but never used         @typescript-eslint/no-unused-vars

... more valid linting
```

I can also mitigate the problem by making the following code change to ignore-pattern.js:
```
function getCommonAncestorPath(sourcePaths) {
    let result = sourcePaths[0];

    for (let i = 1; i < sourcePaths.length; ++i) {
        const a = result;
        const b = sourcePaths[i];

        // Set the shorter one (it's the common ancestor if one includes the other).
        result = a.length < b.length ? a : b;

        // Set the common ancestor.
        for (let j = 0, lastSepPos = 0; j < a.length && j < b.length; ++j) {
            if (a[j] !== b[j]) {
                result = a.slice(0, lastSepPos);
                break;
            }
            if (a[j] === path.sep) {
                lastSepPos = j;
            }
        }
    }
    // this was the previous return statement
    // return result || path.sep;

    // new return statement
    let resolvedResult = result || path.sep;
    if (resolvedResult && resolvedResult.endsWith(":")) {
        resolvedResult = resolvedResult + path.sep;
    }
    return resolvedResult;
}
```


Discussion:
Thanks for the detailed bug report. This does seem like a bug to me. Looking at how paths work in WIndows in [Microsoft's documentation](https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file?redirectedfrom=MSDN#fully-qualified-vs-relative-paths), it does look like we need to fix this algorithm to not remove the separator from `C:`, since `"C:"` is considered relative while `"C:\\"` is considered absolute.
@kaicataldo are you happy with drkestels `//new return statement` from the snippet above or would you prefer just update  `result = a.slice(0, lastSepPos);` to ` result = a.slice(0, lastSepPos + 1);`
This makes sense to me:

```js
    let resolvedResult = result || path.sep;
    if (resolvedResult && resolvedResult.endsWith(":")) {
        resolvedResult = resolvedResult + path.sep;
    }
    return resolvedResult;
```

I'd be curious to hear from other team members who have more experience with Windows think. I'm no expert on Windows path gotchas.
@kaicataldo how about something like this https://github.com/eslint/eslint/pull/13078/files
@nzakas @kaicataldo do you have docs on your release pipeline? I am trying to understand rough timelines around when this will be available to consume from npmjs in an alpha build and then an official release?
The timeline for major releases is a bit ad-hoc, and given the current worldwide situation and team members dealing with that and other personal issues, I can’t really give you any specify guidance. We wanted to get 7.0.0 out by May, and the next alpha was supposed to be last week but we couldn’t get stuff done due to low availability.
no probs, completely understand the current situation was just curious so i can plan around it.  take care, nick.
@nickharris v7.0.0 was released last Friday!
yup already using it :) thank you!



Patch:
diff --git a/lib/cli-engine/config-array/ignore-pattern.js b/lib/cli-engine/config-array/ignore-pattern.js
index 92690b9f8ae3..6eaec4258e1a 100644
--- a/lib/cli-engine/config-array/ignore-pattern.js
+++ b/lib/cli-engine/config-array/ignore-pattern.js
@@ -71,7 +71,13 @@ function getCommonAncestorPath(sourcePaths) {
         }
     }
 
-    return result || path.sep;
+    let resolvedResult = result || path.sep;
+
+    // if Windows common ancestor is root of drive must have trailing slash to be absolute.
+    if (resolvedResult && resolvedResult.endsWith(":") && process.platform === "win32") {
+        resolvedResult += path.sep;
+    }
+    return resolvedResult;
 }
 
 /**


Test patch:
diff --git a/tests/lib/cli-engine/config-array/ignore-pattern.js b/tests/lib/cli-engine/config-array/ignore-pattern.js
index 443d350a2ed0..9d652f3b6896 100644
--- a/tests/lib/cli-engine/config-array/ignore-pattern.js
+++ b/tests/lib/cli-engine/config-array/ignore-pattern.js
@@ -6,6 +6,7 @@
 
 const assert = require("assert");
 const path = require("path");
+const sinon = require("sinon");
 const { IgnorePattern } = require("../../../../lib/cli-engine/config-array/ignore-pattern");
 
 describe("IgnorePattern", () => {
@@ -52,74 +53,127 @@ describe("IgnorePattern", () => {
 
     describe("static createIgnore(ignorePatterns)", () => {
         describe("with two patterns should return a function, and the function", () => {
-            const cwd = process.cwd();
-            const basePath1 = path.join(cwd, "foo/bar");
-            const basePath2 = path.join(cwd, "abc/");
-            const ignores = IgnorePattern.createIgnore([
-                new IgnorePattern(["*.js", "/*.ts", "!a.*", "!/b.*"], basePath1),
-                new IgnorePattern(["*.js", "/*.ts", "!a.*", "!/b.*"], basePath2)
-            ]);
-            const patterns = [
-                ["a.js", false],
-                ["a.ts", false],
-                ["b.js", false],
-                ["b.ts", false],
-                ["c.js", false],
-                ["c.ts", false],
-                ["dir/a.js", false],
-                ["dir/a.ts", false],
-                ["dir/b.js", false],
-                ["dir/b.ts", false],
-                ["dir/c.js", false],
-                ["dir/c.ts", false],
-                ["foo/bar/a.js", false],
-                ["foo/bar/a.ts", false],
-                ["foo/bar/b.js", false],
-                ["foo/bar/b.ts", false],
-                ["foo/bar/c.js", true],
-                ["foo/bar/c.ts", true],
-                ["foo/bar/dir/a.js", false],
-                ["foo/bar/dir/a.ts", false],
-                ["foo/bar/dir/b.js", true],
-                ["foo/bar/dir/b.ts", false],
-                ["foo/bar/dir/c.js", true],
-                ["foo/bar/dir/c.ts", false],
-                ["abc/a.js", false],
-                ["abc/a.ts", false],
-                ["abc/b.js", false],
-                ["abc/b.ts", false],
-                ["abc/c.js", true],
-                ["abc/c.ts", true],
-                ["abc/dir/a.js", false],
-                ["abc/dir/a.ts", false],
-                ["abc/dir/b.js", true],
-                ["abc/dir/b.ts", false],
-                ["abc/dir/c.js", true],
-                ["abc/dir/c.ts", false]
-            ];
-
-            for (const [filename, expected] of patterns) {
-                it(`should return ${expected} if '${filename}' was given.`, () => {
-                    assert.strictEqual(ignores(path.join(cwd, filename)), expected);
+
+            /**
+             * performs static createIgnre assertions against the cwd.
+             * @param {string} cwd cwd to be the base path for assertions
+             * @returns {void}
+             */
+            function assertions(cwd) {
+                const basePath1 = path.join(cwd, "foo/bar");
+                const basePath2 = path.join(cwd, "abc/");
+                const ignores = IgnorePattern.createIgnore([
+                    new IgnorePattern(["*.js", "/*.ts", "!a.*", "!/b.*"], basePath1),
+                    new IgnorePattern(["*.js", "/*.ts", "!a.*", "!/b.*"], basePath2)
+                ]);
+                const patterns = [
+                    ["a.js", false],
+                    ["a.ts", false],
+                    ["b.js", false],
+                    ["b.ts", false],
+                    ["c.js", false],
+                    ["c.ts", false],
+                    ["dir/a.js", false],
+                    ["dir/a.ts", false],
+                    ["dir/b.js", false],
+                    ["dir/b.ts", false],
+                    ["dir/c.js", false],
+                    ["dir/c.ts", false],
+                    ["foo/bar/a.js", false],
+                    ["foo/bar/a.ts", false],
+                    ["foo/bar/b.js", false],
+                    ["foo/bar/b.ts", false],
+                    ["foo/bar/c.js", true],
+                    ["foo/bar/c.ts", true],
+                    ["foo/bar/dir/a.js", false],
+                    ["foo/bar/dir/a.ts", false],
+                    ["foo/bar/dir/b.js", true],
+                    ["foo/bar/dir/b.ts", false],
+                    ["foo/bar/dir/c.js", true],
+                    ["foo/bar/dir/c.ts", false],
+                    ["abc/a.js", false],
+                    ["abc/a.ts", false],
+                    ["abc/b.js", false],
+                    ["abc/b.ts", false],
+                    ["abc/c.js", true],
+                    ["abc/c.ts", true],
+                    ["abc/dir/a.js", false],
+                    ["abc/dir/a.ts", false],
+                    ["abc/dir/b.js", true],
+                    ["abc/dir/b.ts", false],
+                    ["abc/dir/c.js", true],
+                    ["abc/dir/c.ts", false]
+                ];
+
+                for (const [filename, expected] of patterns) {
+                    it(`should return ${expected} if '${filename}' was given.`, () => {
+                        assert.strictEqual(ignores(path.join(cwd, filename)), expected);
+                    });
+                }
+
+                it("should return false if '.dot.js' and false was given.", () => {
+                    assert.strictEqual(ignores(path.join(cwd, ".dot.js"), false), true);
                 });
-            }
 
-            it("should return false if '.dot.js' and false was given.", () => {
-                assert.strictEqual(ignores(path.join(cwd, ".dot.js"), false), true);
-            });
+                it("should return true if '.dot.js' and true were given.", () => {
+                    assert.strictEqual(ignores(path.join(cwd, ".dot.js"), true), false);
+                });
 
-            it("should return true if '.dot.js' and true were given.", () => {
-                assert.strictEqual(ignores(path.join(cwd, ".dot.js"), true), false);
-            });
+                it("should return false if '.dot/foo.js' and false was given.", () => {
+                    assert.strictEqual(ignores(path.join(cwd, ".dot/foo.js"), false), true);
+                });
 
+                it("should return true if '.dot/foo.js' and true were given.", () => {
+                    assert.strictEqual(ignores(path.join(cwd, ".dot/foo.js"), true), false);
+                });
+            }
 
-            it("should return false if '.dot/foo.js' and false was given.", () => {
-                assert.strictEqual(ignores(path.join(cwd, ".dot/foo.js"), false), true);
-            });
+            assertions(process.cwd());
+
+            /*
+             * This will catch regressions of Windows specific issue #12850 when run on CI nodes.
+             * This runs the full set of assertions for the function returned from IgnorePattern.createIgnore.
+             * When run on Windows CI nodes the .root drive i.e C:\ will be supplied
+             *  forcing getCommonAncestors to resolve to the root of the drive thus catching any regrssion of 12850.
+             * When run on *nix CI nodes provides additional coverage on this OS too.
+             *  assertions when run on Windows CI nodes and / on *nix OS
+             */
+            assertions(path.parse(process.cwd()).root);
+        });
+    });
 
-            it("should return true if '.dot/foo.js' and true were given.", () => {
-                assert.strictEqual(ignores(path.join(cwd, ".dot/foo.js"), true), false);
-            });
+    describe("static createIgnore(ignorePatterns)", () => {
+
+        /*
+         * This test will catch regressions of Windows specific issue #12850 when run on your local dev box
+         * irrespective of if you are running a Windows or *nix style OS.
+         * When running on *nix sinon is used to emulate Windows behaviors of path and platform APIs
+         * thus ensuring that the Windows specific fix is exercised and any regression is caught.
+         */
+        it("with common ancestor of drive root on windows should not throw", () => {
+            try {
+
+                /*
+                 * When not on Windows return win32 values so local runs on *nix hit the same code path as on Windows
+                 * thus enabling developers with *nix style OS to catch and debug any future regression of #12850 without
+                 * requiring a Windows based OS.
+                 */
+                if (process.platform !== "win32") {
+                    sinon.stub(process, "platform").value("win32");
+                    sinon.stub(path, "sep").value(path.win32.sep);
+                    sinon.replace(path, "isAbsolute", path.win32.isAbsolute);
+                }
+
+                const ignores = IgnorePattern.createIgnore([
+                    new IgnorePattern(["*.js"], "C:\\foo\\bar"),
+                    new IgnorePattern(["*.js"], "C:\\abc\\")
+                ]);
+
+                // calls to this should not throw when getCommonAncestor returns root of drive
+                ignores("C:\\abc\\contract.d.ts");
+            } finally {
+                sinon.restore();
+            }
         });
     });
 });