Skip to content

Commit

Permalink
fix(core): improve cacheKey when using flat config
Browse files Browse the repository at this point in the history
This change improves the logic for generating the cache key used for both the exportMap cache and resolver cache when using flat config.  Prior to this change, the cache key was a combination of the `parserPath`, a hash of the `parserOptions`, a hash of the plugin settings, and the path of the file.  When using flat config, `parserPath` isn't provided.  So, there's the possibility of incorrect cache objects being used if someone ran with this plugin using different parsers within the same lint execution, if parserOptions and settings are the same.  The equivalent cacheKey construction when using flat config is to use `languageOptions` as a component of the key, rather than `parserPath` and `parserOptions`.  One caveat is that the `parser` property of `languageOptions` is an object that oftentimes has the same two functions (`parse` and `parseForESLint`).  This won't be reliably distinct when using the base JSON.stringify function to detect changes.  So, this implementation uses a replace function along with `JSON.stringify` to stringify function properties along with other property types.

To ensure that this will work properly with v9, I also tested this against
  • Loading branch information
michaelfaith committed Sep 26, 2024
1 parent 6218a8a commit 8136e96
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 7 deletions.
35 changes: 29 additions & 6 deletions src/exportMap/childContext.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { hashObject } from 'eslint-module-utils/hash';

let parserOptionsHash = '';
let prevParserOptions = '';
let optionsHash = '';
let prevOptions = '';
let settingsHash = '';
let prevSettings = '';

// Replacer function helps us with serializing the parser nested within `languageOptions`.
function stringifyReplacerFn(_, value) {
if (typeof value === 'function') {
return value.toString();
}
return value;
}

/**
* don't hold full context object in memory, just grab what we need.
* also calculate a cacheKey, where parts of the cacheKey hash are memoized
Expand All @@ -17,13 +25,28 @@ export default function childContext(path, context) {
prevSettings = JSON.stringify(settings);
}

if (JSON.stringify(parserOptions) !== prevParserOptions) {
parserOptionsHash = hashObject({ parserOptions }).digest('hex');
prevParserOptions = JSON.stringify(parserOptions);
// We'll use either a combination of `parserOptions` and `parserPath` or `languageOptions`
// to construct the cache key, depending on whether this is using a flat config or not.
let optionsToken;
if (!parserPath && languageOptions) {
if (JSON.stringify(languageOptions, stringifyReplacerFn) !== prevOptions) {
optionsHash = hashObject({ languageOptions }).digest('hex');
prevOptions = JSON.stringify(languageOptions, stringifyReplacerFn);
}
// For languageOptions, we're just using the hashed options as the options token
optionsToken = optionsHash;
} else {
if (JSON.stringify(parserOptions) !== prevOptions) {
optionsHash = hashObject({ parserOptions }).digest('hex');
prevOptions = JSON.stringify(parserOptions);
}
// When not using flat config, we use a combination of the hashed parserOptions
// and parserPath as the token
optionsToken = String(parserPath) + optionsHash;
}

return {
cacheKey: String(parserPath) + parserOptionsHash + settingsHash + String(path),
cacheKey: optionsToken + settingsHash + String(path),
settings,
parserOptions,
parserPath,
Expand Down
72 changes: 71 additions & 1 deletion tests/src/exportMap/childContext.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect } from 'chai';
import { hashObject } from 'eslint-module-utils/hash';

import childContext from '../../../src/exportMap/childContext';

Expand All @@ -16,8 +17,13 @@ describe('childContext', () => {
const languageOptions = {
ecmaVersion: 2024,
sourceType: 'module',
parser: {},
parser: {
parseForESLint() { return 'parser1'; },
},
};
const languageOptionsHash = hashObject({ languageOptions }).digest('hex');
const parserOptionsHash = hashObject({ parserOptions }).digest('hex');
const settingsHash = hashObject({ settings }).digest('hex');

// https://github.com/import-js/eslint-plugin-import/issues/3051
it('should pass context properties through, if present', () => {
Expand Down Expand Up @@ -48,4 +54,68 @@ describe('childContext', () => {
expect(result.path).to.equal(path);
expect(result.cacheKey).to.be.a('string');
});

it('should construct cache key out of languageOptions if present', () => {
const mockContext = {
settings,
languageOptions,
};

const result = childContext(path, mockContext);

expect(result.cacheKey).to.equal(languageOptionsHash + settingsHash + path);
});

it('should use the same cache key upon multiple calls', () => {
const mockContext = {
settings,
languageOptions,
};

let result = childContext(path, mockContext);

const expectedCacheKey = languageOptionsHash + settingsHash + path;
expect(result.cacheKey).to.equal(expectedCacheKey);

result = childContext(path, mockContext);
expect(result.cacheKey).to.equal(expectedCacheKey);
});

it('should update cacheKey if different languageOptions are passed in', () => {
const mockContext = {
settings,
languageOptions,
};

let result = childContext(path, mockContext);

const firstCacheKey = languageOptionsHash + settingsHash + path;
expect(result.cacheKey).to.equal(firstCacheKey);

// Second run with different parser function
mockContext.languageOptions = {
...languageOptions,
parser: {
parseForESLint() { return 'parser2'; },
},
};

result = childContext(path, mockContext);

const secondCacheKey = hashObject({ languageOptions: mockContext.languageOptions }).digest('hex') + settingsHash + path;
expect(result.cacheKey).to.not.equal(firstCacheKey);
expect(result.cacheKey).to.equal(secondCacheKey);
});

it('should construct cache key out of parserOptions and parserPath if no languageOptions', () => {
const mockContext = {
settings,
parserOptions,
parserPath,
};

const result = childContext(path, mockContext);

expect(result.cacheKey).to.equal(String(parserPath) + parserOptionsHash + settingsHash + path);
});
});

0 comments on commit 8136e96

Please sign in to comment.