Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add --cache option to lint only changed files #1763

Merged
merged 26 commits into from
Sep 27, 2021

Conversation

mad-gooze
Copy link
Contributor

@mad-gooze mad-gooze commented Sep 24, 2021

Hi!

This PR adds three new cli options:

  • --cache that enables caching lint results on disk
  • --cache-location <path> that sets cache file location
  • --cache-strategy <metadata|content> that sets cache strategy used by file-entry-cache

Cache implementation is heavily inspired by ESLint https://github.com/eslint/eslint/blob/c981fb1994cd04914042ced1980aa86b68ba7be9/lib/cli-engine/lint-result-cache.js

@mad-gooze mad-gooze changed the title add --cache option to check only changed files add --cache option to lint only changed files Sep 24, 2021
@mad-gooze mad-gooze marked this pull request as ready for review September 25, 2021 02:18
@Jason3S
Copy link
Collaborator

Jason3S commented Sep 25, 2021

Wow, you have been busy! I was hoping to add a cache.
It is going to take me a bit to look through this. I need to try it out!

I was thinking it would be a good idea to keep all cspell related files in the same directory: .cspell

- .cspell/
    |-- .cspellcache/
    |-- custom-dictionary.txt
    |-- known-issues.yaml
    |-- future files.

@Jason3S
Copy link
Collaborator

Jason3S commented Sep 25, 2021

Seems to be breaking on the Yarn pnp.js files.

./bin.js lint --cache --cache-strategy content --no-progress "**"

image

Hmm. I don't think it is Yarn related:

./bin.js lint --cache --cache-strategy content --no-progress --exclude "**/yarn2/**" "**":

image

@Jason3S
Copy link
Collaborator

Jason3S commented Sep 25, 2021

In package.json there is a test-bin script. I suggest adding two tests to that, one for each strategy.

    "test-bin": "... 
    "test-bin-spell-cache-content": "node ./bin.js -c cspellrc.json --cache --cache-strategy content --cache-location ...",
    "test-bin-spell-cache-meta": "node ./bin.js -c cspellrc.json --cache --cache-strategy metadata --cache-location ...",

Copy link
Collaborator

@Jason3S Jason3S left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for adding this.

I think it will help a lot.

I think it is worth it to get this in with its current logic.
Note: the cache logic is not correct for a mono-repo style project (like this one) with nested cspell.json files. In a future PR, it might be worth it to handle the case when a nested config file changes.

packages/cspell/src/util/cache/DiskCache.ts Outdated Show resolved Hide resolved
packages/cspell/src/lint.ts Outdated Show resolved Hide resolved
packages/cspell/src/options.ts Outdated Show resolved Hide resolved
packages/cspell/src/lint.ts Show resolved Hide resolved
@Jason3S Jason3S changed the title add --cache option to lint only changed files feat: add --cache option to lint only changed files Sep 25, 2021
@mad-gooze
Copy link
Contributor Author

Note: the cache logic is not correct for a mono-repo style project (like this one) with nested cspell.json files. In a future PR, it might be worth it to handle the case when a nested config file changes.

Oh, really, i did not get the idea that cspell supports nested configs.
I think I can handle it easily here by hashing ConfigInfo for every file (in a way it's done in eslint)

@Jason3S
Copy link
Collaborator

Jason3S commented Sep 26, 2021

I'm looking forward to getting this in.

I you would like, we can do it in two steps.

  1. Fix the current PR so everything passes and I will merge it.
  2. Follow up on removing .doc and the per file level config in subsequent PRs.

@mad-gooze
Copy link
Contributor Author

I've removed doc and added per-file config hashing.

Could you review this again please?

Copy link
Collaborator

@Jason3S Jason3S left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for getting this done!

}));

const mockReadFileInfo = jest.spyOn(fileHelper, 'readFileInfo');
jest.mock('../../fileHelper', () => ({ readFileInfo: jest.fn() }));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: It is also possible to use the spy

// jest.mock('../../fileHelper', () => ({ readFileInfo: jest.fn() }));
mockReadFileInfo.mockImplementation(jest.fn());

getConfigHash: jest.fn().mockReturnValue('TEST_CONFIG_HASH'),
}));

const mockCreateFileEntryCache = jest.spyOn(FileEntryCacheModule, 'create');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule of thumb:

  1. Mock Module.
  2. Create Spy.
jest.mock('file-entry-cache');
const mockCreateFileEntryCache = jest.spyOn(FileEntryCacheModule, 'create');
mockCreateFileEntryCache.mockImplementation(
    jest.fn().mockReturnValue({
        getFileDescriptor: jest.fn(),
        reconcile: jest.fn(),
    })
);

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also worth reading:
mocked - Test helpers | ts-jest


beforeEach(() => {
diskCache = new DiskCache('.foobar', false);
fileEntryCache = mockCreateFileEntryCache.mock.results[0].value;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fileEntryCache = mockCreateFileEntryCache.mock.results[0].value;

Accessing the mock like this is fragile because it depends upon Jest's implementation.
By flipping the spy logic and injecting a known cache entry, it will make things more reliable.

    const fileEntryCache = {
        getFileDescriptor: jest.fn(),
        reconcile: jest.fn(),
    };

    beforeEach(() => {
        diskCache = new DiskCache('.foobar', false);
        mockCreateFileEntryCache.mockImplementation(jest.fn().mockReturnValue(fileEntryCache));
    });


public async getCachedLintResults(filename: string, configInfo: ConfigInfo): Promise<FileResult | undefined> {
const fileDescriptor = this.fileEntryCache.getFileDescriptor(filename);
const meta = fileDescriptor.meta as CSpellCacheMeta;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Watch out for the cast. It removed the undefined from the type.
meta can be undefined, but it is not getting checked.

        const meta = fileDescriptor.meta as CSpellCacheMeta | undefined;

or

type CSpellCacheMeta =
    | (FileDescriptor['meta'] & {
          result: CachedFileResult;
          configHash: string;
      })
    | undefined;

return;
}

const meta = fileDescriptor.meta as CSpellCacheMeta;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above. meta can be undefined.

jest.mock('./hash', () => ({
hash: jest.fn().mockReturnValue('TEST_HASH'),
}));
jest.mock('../../../package.json', () => ({ version: '0.0.0' }));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading the package.json is also an option to ensure the version is correct.

// eslint-disable-next-line @typescript-eslint/no-var-requires
const packageVersion = require('../../../package.json').version;

// ...

expect(mockHash.mock.calls[0][0]).toContain(packageVersion);

@Jason3S
Copy link
Collaborator

Jason3S commented Sep 27, 2021

@mad-gooze,

Thank you very much! The suggestions I made can come in later PRs.

@Jason3S Jason3S merged commit 4bdfd09 into streetsidesoftware:main Sep 27, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants