DEV Community

coderaiser
coderaiser

Posted on

What's new in JavaScript Linter 🐊Putout v42

image

नित्य आत्माव्यय: शुद्ध: सर्वग: सर्ववित्पर: ।
धत्तेऽसावात्मनो लिङ्गं मायया विसृजन्गुणान् ॥ २२ ॥

nitya ātmāvyayaḥ śuddhaḥ
sarvagaḥ sarva-vit paraḥ
dhatte ’sāv ātmano liṅgaṁ
māyayā visṛjan guṇān

The spirit soul, the living entity, has no death, for he is eternal and inexhaustible. Being free from material contamination, he can go anywhere in the material or spiritual worlds. He is fully aware and completely different from the material body, but because of being misled by misuse of his slight independence, he is obliged to accept subtle and gross bodies created by the material energy and thus be subjected to so-called material happiness and distress. Therefore, no one should lament for the passing of the spirit soul from the body.

(c) Śrīmad-Bhāgavatam 7.2.22

Hi folks 🎈!
The time is come for a new major release of 🐊Putout, pluggable and configurable JavaScript Linter, code transformer and formatter. This release has a couple breaking changes and some new features, so get a cup of hot tea ☕️ and enjoy reading!

📛 transform, transformAsync, findPlaces and findPlacesAsync no longer requires source argument

When you run putout('const a = 3') behind the scene there is a couple operations: parse, transform and print.

For a long time signature of transform looked this way:

transform(ast, source, options);
Enter fullscreen mode Exit fullscreen mode

Why do we need source just after parse? - you ask. To keep tracking of shebang:

In computing shebang is the character sequence #!, consisting of the characters number sign (also known as sharp or hash) and exclamation mark (also known as bang), at the beginning of a script.

(c) wiki

So all flow now looks like this:

const source = parse(ast);
transform(ast, options);
const code = print(ast);
Enter fullscreen mode Exit fullscreen mode

For a long time parsers like acorn, Babel and Espree (of ESLint) could not parse it, but with Hashbang Proposal everything changed, also recast couldn't handle hashbang but it is not longer supported so
there is not more need of such a strange way to keep line numbers correct 🎉.

As usual all changes handled by putout/remove-useless-source-argument.

📛 @putout/filesystem a bit simplified

@putout/plugin-filesystem a bit simplified:

{
    "rules": {
-       "filesystem/remove-travis-yml-file": "off",
-       "filesystem/remove-vim-swap-file": "off",
-       "filesystem/remove-ds-store-file": "off",
-       "filesystem/remove-empty-directory": "off",
-       "filesystem/remove-nyc-output": "off",
+       "filesystem/remove-files": "off",
+       "coverage/remove-files": "off",
  }
}
Enter fullscreen mode Exit fullscreen mode

If you changed .putout.json all modifications is handled by @putout/plugin-putout-config 😏.

📛 Changes in default ignore

Default ignores changed:

{
  "ignore": [
        "**/*.lock",
        "**/*.log",
        "**/.nyc_output/*",
        "**/.yarn",
        "**/.pnp.*",
        "**/.idea",
        "**/.git",
        "**/package-lock.json",
        "**/node_modules",
        "**/fixture",
-      "**/coverage",
+      "**/coverage/*",
        "**/dist",
        "**/dist-dev",
        "**/build"
    ],
}
Enter fullscreen mode Exit fullscreen mode

Now 🐊Putout sees coverage as directory, but ignores any files inside:

total 32136
drwxr-xr-x  13 coderaiser  staff   416B Feb 17 16:23 ./
drwxr-xr-x   3 coderaiser  staff    96B Feb 17 16:23 ../
-rw-------   1 coderaiser  staff   6.7M Feb 17 16:23 coverage-95233-1771338202619-0.json
-rw-------   1 coderaiser  staff    88K Feb 17 16:23 coverage-95234-1771338198859-0.json
-rw-------   1 coderaiser  staff   1.3M Feb 17 16:23 coverage-95235-1771338199593-1.json
-rw-------   1 coderaiser  staff   460K Feb 17 16:23 coverage-95235-1771338199605-0.json
-rw-------   1 coderaiser  staff   1.3M Feb 17 16:23 coverage-95236-1771338199923-1.json
-rw-------   1 coderaiser  staff   460K Feb 17 16:23 coverage-95236-1771338199936-0.json
-rw-------   1 coderaiser  staff   1.3M Feb 17 16:23 coverage-95237-1771338200265-1.json
-rw-------   1 coderaiser  staff   384K Feb 17 16:23 coverage-95237-1771338200276-0.json
-rw-------   1 coderaiser  staff   1.2M Feb 17 16:23 coverage-95239-1771338200538-0.json
-rw-------   1 coderaiser  staff   1.2M Feb 17 16:23 coverage-95240-1771338200803-0.json
-rw-------   1 coderaiser  staff   1.2M Feb 17 16:23 coverage-95241-1771338201056-0.json
Enter fullscreen mode Exit fullscreen mode

We don't want to lint them, but RedLint needs to see coverage directory so he can remove it with @putout/coverage/remove-files

image

we will came back to this is in a couple minutes.

📛 Migrated @putout/operator-ignore to Traverser

A .gitignore file specifies intentionally untracked files that Git should ignore.

(c) git-scm.com

@putout/operator-ignore simplifies creating ignore-plugins.

It was Replacer from the beginning, it is one of the easiest plugins to write, it is always had report and replace, but can also contain match to filter nodes that we want to ignore. Here is how it looked like:

export const ignore = (type, {name, property, list}) => {
    const [, collector] = type.split(/[()]/);

    return {
        report: createReport(name),
        match: createMatch({
            type,
            property,
            collector,
            list,
        }),
        replace: createReplace({
            type,
            property,
            collector,
            list,
        }),
    };
};
const createMatch = ({type, property, collector, list}) => ({options}) => {
    const {dismiss = []} = options;
    const newNames = filterNames(list, dismiss);

    return {
        [type]: (vars) => {
            const elements = parseElements(vars, {
                property,
                collector,
            });

            if (!elements)
                return false;

            const list = elements.map(getValue);

            for (const name of newNames) {
                if (!list.includes(name))
                    return true;
            }

            return false;
        },
    };
};

const createReplace = ({type, property, collector, list}) => ({options}) => {
    const {dismiss = []} = options;
    const newNames = filterNames(list, dismiss);

    return {
        [type]: (vars, path) => {
            const elements = parseElements(vars, {
                property,
                collector,
            });

            const list = elements.map(getValue);

            for (const name of newNames) {
                if (!list.includes(name))
                    elements.push(stringLiteral(name));
            }

            return path;
        },
    };
};
Enter fullscreen mode Exit fullscreen mode

As you can see part of code is duplicated. It runs twice:

  • to filter things out;
  • to replace things up;

In such cases much better to use Traverser.

Now what we have is:

const difference = (a, b) => new Set(a).difference(new Set(b));

export const ignore = ({name, property, list, type = __ignore}) => ({
    report: createReport(name),
    fix,
    traverse: createTraverse({
        type,
        property,
        list,
    }),
});

const addQuotes = (a) => `'${a}'`;

const createReport = (filename) => ({name, matchedElements}) => {
    let insteadOf = '';

    if (matchedElements.length) {
        const replacedNames = matchedElements.map(getValue);
        const namesLine = replacedNames
            .map(addQuotes)
            .join(', ');

        insteadOf = ` instead of ${namesLine}`;
    }

    return `Add '${name}'${insteadOf} to '${filename}'`;
};

export const fix = ({path, name, matchedElements}) => {
    path.node.elements.push(stringLiteral(name));
    matchedElements.map(remove);
};

const createTraverse = ({type, property, list}) => ({push, options}) => {
    const {dismiss = []} = options;
    const newNames = filterNames(list, dismiss);

    if (!newNames.length)
        return {};

    return {
        [type]: (path) => {
            const [parentOfElements, elements] = parseElements(path, {
                property,
            });

            if (!parentOfElements)
                return;

            const list = elements.map(getValue);

            for (const name of difference(newNames, list)) {
                const match = picomatch(name);
                const matchedElements = [];

                for (const current of elements.filter(exists)) {
                    const {value} = current.node;

                    if (match(value))
                        matchedElements.push(current);
                }

                push({
                    path: parentOfElements,
                    matchedElements,
                    elements,
                    name,
                });
            }
        },
    };
};
Enter fullscreen mode Exit fullscreen mode

It has a couple benefits:

  • ✅ We have more control over report;
  • ✅ We have no duplication, all checks works only once in traverse and if it is OK - fix;

One of the new features for @putout/operator-ignore is when you add mask *.log and your ignore file already has yarn-error.log, this both values are merged (that's right! not inserted and then with other rule cleaned up, but merged).

Since the best way to modify ignore files is using group rules - it is made in operator. If you have any kind of concerns about it - I'll be happy to discuss 😏.

☝️ Take away: you should always use the simplest possible way to write transformation, and later, when you have tests written, and see places to improve - refactor and make code better 😏.
☝️ As always, this case is handled for you in @putout/plugin-putout and will be auto fixed on next lint 😏

📛 Dropped support of ESLint < v10

eslint-plugin-putout requires ESLint >= v10 it is just released, so is better to upgrade.

Babel also already supports it, so the time is come 🤷‍♂️.

🔥crawlFile - new way of searching files

When you need to find files related to other files like in esm/apply-name-to-imported-file you can end up with:

export const report = () => 'Add file';
export const fix = (file) => {
    writeFileContent(file, 'hello');
};

export const scan = (rootPath, {push, trackFile}) => {
    for (const file of trackFile(rootPath, 'hello.txt')) {
        findFile(rootPath, 'again and again');
    }
};
Enter fullscreen mode Exit fullscreen mode

It will be very slow! And you can use crawlDirectory() instead:

export const report = () => 'Add file';
export const fix = (file) => {
    writeFileContent(file, 'hello');
};

export const scan = (rootPath, {push, trackFile}) => {
    const crawled = crawlDirectory(rootPath);

    for (const file of trackFile(rootPath, 'hello.txt')) {
        findFile(rootPath, 'again and again', {
            crawled,
        });
    }
};
Enter fullscreen mode Exit fullscreen mode

But it is easy to forget, or remove during refactoring, since everything will work as before, only twice slower on big amount of files.

So you better use crawlFile:

export const report = () => 'Add file';
export const fix = (file) => {
    writeFileContent(file, 'hello');
};

export const scan = (rootPath, {push, trackFile, crawlFile}) => {
    for (const file of trackFile(rootPath, 'hello.txt')) {
        const files = crawlFile(rootPath, 'no matter how many times');
    }
};
Enter fullscreen mode Exit fullscreen mode

It works much faster, use it when you do not mutate filesystem tree: neither remove nor rename files.

🔥@putout/operator-remove-files

Welcome new operator in a hood!

When you need to create a new plugin for RedLint that removes some kind of files just use:

import {operator} from 'putout';

const {removeFiles} = operator;

export const {
    report,
    fix,
    scan,
} = removeFiles(['.DS_Store']);
Enter fullscreen mode Exit fullscreen mode

It gives ability to write such rules as coverage/remove-files easily.

You still have ability to set files to you want to remove in .putout.json:

{
    "match": {
         ".filesystem.json": {
            "filesystem/remove-files": ["on", {
                "names": ["coverage"]
            }],
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

But also coverage/remove-files handles files related to coverage for you.

🔥@putout/operator-rename-properties

When you need to do some minor modifications to config files, like:

{
    "rules": {
-       "filesystem/remove-nyc-output": "off"
+       "coverage/remove-files": "off"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

All you need is:

import {operator} from 'putout';

const {renameProperties} = operator;

export const {
    report,
    fix,
    traverse,
} = renameProperties([
    ['filesystem/remove-nyc-output', 'coverage/remove-files'],
]);
Enter fullscreen mode Exit fullscreen mode

Checkout in 🐊Putout Editor.

🔥@putout/operator-sort-ignore

When you need to sort things up

ignore

from:

node_modules
*.swp
yarn-error.log
yarn.lock
.idea
.DS_Store
deno.lock

coverage
.filesystem.json
Enter fullscreen mode Exit fullscreen mode

to:

.idea
.filesystem.json
.DS_Store

*.swp

yarn-error.log
yarn.lock
deno.lock

node_modules
coverage
Enter fullscreen mode Exit fullscreen mode

use:

import {operator} from 'putout';

const {sortIgnore} = operator;

export const {
    report,
    fix,
    traverse,
} = sortIgnore({
    name: '.gitignore',
});
Enter fullscreen mode Exit fullscreen mode

json

And to sort up JSON:

{
    "ignore": [
        "**/package-lock.json",
        "**/*.lock",
        "**/.git",
        "**/*.log",
        "**/node_modules"
    ]
}
Enter fullscreen mode Exit fullscreen mode

to

{
    "ignore": [
        "**/*.lock",
        "**/*.log",
        "**/.git",
        "**/package-lock.json",
        "**/node_modules"
    ],
}
Enter fullscreen mode Exit fullscreen mode

Use:

import {operator} from 'putout';

const {sortIgnore, __json} = operator;

export const {
    report,
    fix,
    traverse,
} = sortIgnore({
    name: '.gitignore',
    type: __json,
    property: 'ignore',
});
Enter fullscreen mode Exit fullscreen mode

🔥Totally ESM

🐊Putout now 100% ESM, since it is:

Linter that do not afraid to act like codemod

With help of:

You can migrate any kind of JavaScript codebase from CommonJS to ESM.

Here is the algorithm:

  1. Change type of your package.json from commonjs to module;
  2. Run putout --fix ., it will fix any code that uses require and module.exports to import and export;
  3. Run redlint fix, it will resolve import names and change import {readFile} from './reader' to import {readFile} from './reader.js' with help of Scanners introduced in Putout v34;

That's it!

If you avoid any kinds of hacks, like:

using mock-require:

❌ Example of incorrect code

// mock modules worked in CommonJS, but not in ESM
mockRequire('fs', {
    readFile,
});

run(code);
Enter fullscreen mode Exit fullscreen mode

✅ Example of correct code

// use dependency injection instead
run(code, {
    readFile
});
Enter fullscreen mode Exit fullscreen mode

using same names for exported and internal functions:

❌ Example of incorrect code

// 'run' already declared so we cannot just use 'const run = () => {run();}'
module.exports.run = () => {
    return run();
};

function run() {
}
Enter fullscreen mode Exit fullscreen mode

✅ Example of correct code

// easy to migrate to `export const run = () => {return runAll();}`, since no overlap with names
module.exports.run = () => {
    return runAll();
};

function runAll() {
}
Enter fullscreen mode Exit fullscreen mode

and reserved words

❌ Example of incorrect code

// 'delete is reserved word, you cannot use it to name variable `export const delete = () => {}`
module.exports.delete = () => {};
Enter fullscreen mode Exit fullscreen mode

✅ Example of correct code

// it would be 'export const remove = () => {}`, so no problem at all
module.exports.remove = () => {}
Enter fullscreen mode Exit fullscreen mode

If you check your code before migration, or a bit fix after migration to avoid such patterns, it will be much easier!
Any ways 🐊Putout always here to help you with any kind of your migrations 😏.

That's all for today, have a nice evening 🐈!

Top comments (0)