nx/e2e/linter/src/linter.test.ts

939 lines
31 KiB
TypeScript

import * as path from 'path';
import {
checkFilesDoNotExist,
checkFilesExist,
cleanupProject,
createFile,
getSelectedPackageManager,
newProject,
readFile,
readJson,
runCLI,
runCreateWorkspace,
uniq,
updateFile,
updateJson,
} from '@nx/e2e/utils';
import * as ts from 'typescript';
/**
* Importing this helper from @typescript-eslint/type-utils to ensure
* compatibility with TS < 4.8 due to the API change in TS4.8.
* This helper allows for support of TS <= 4.8.
*/
import { getModifiers } from '@typescript-eslint/type-utils';
describe('Linter', () => {
describe('Integrated', () => {
const myapp = uniq('myapp');
const mylib = uniq('mylib');
let projScope;
beforeAll(() => {
projScope = newProject();
runCLI(`generate @nx/react:app ${myapp} --tags=validtag`);
runCLI(`generate @nx/js:lib ${mylib}`);
});
afterAll(() => cleanupProject());
describe('linting errors', () => {
let defaultEslintrc;
beforeAll(() => {
updateFile(`apps/${myapp}/src/main.ts`, `console.log("should fail");`);
defaultEslintrc = readJson('.eslintrc.json');
});
afterEach(() => {
updateFile('.eslintrc.json', JSON.stringify(defaultEslintrc, null, 2));
});
it('should check for linting errors', () => {
// create faulty file
updateFile(`apps/${myapp}/src/main.ts`, `console.log("should fail");`);
const eslintrc = readJson('.eslintrc.json');
// set the eslint rules to error
eslintrc.overrides.forEach((override) => {
if (override.files.includes('*.ts')) {
override.rules['no-console'] = 'error';
}
});
updateFile('.eslintrc.json', JSON.stringify(eslintrc, null, 2));
// 1. linting should error when rules are not followed
let out = runCLI(`lint ${myapp}`, { silenceError: true });
expect(out).toContain('Unexpected console statement');
// 2. linting should not error when rules are not followed and the force flag is specified
expect(() => runCLI(`lint ${myapp} --force`)).not.toThrow();
eslintrc.overrides.forEach((override) => {
if (override.files.includes('*.ts')) {
override.rules['no-console'] = undefined;
}
});
updateFile('.eslintrc.json', JSON.stringify(eslintrc, null, 2));
// 3. linting should not error when all rules are followed
out = runCLI(`lint ${myapp}`, { silenceError: true });
expect(out).toContain('All files pass linting');
}, 1000000);
it('should cache eslint with --cache', () => {
function readCacheFile(cacheFile) {
const cacheInfo = readFile(cacheFile);
return process.platform === 'win32'
? cacheInfo.replace(/\\\\/g, '\\')
: cacheInfo;
}
// should generate a default cache file
expect(() => checkFilesExist(`.eslintcache`)).toThrow();
runCLI(`lint ${myapp} --cache`, { silenceError: true });
expect(() => checkFilesExist(`.eslintcache`)).not.toThrow();
expect(readCacheFile(`.eslintcache`)).toContain(
path.normalize(`${myapp}/src/app/app.spec.tsx`)
);
// should let you specify a cache file location
expect(() => checkFilesExist(`my-cache`)).toThrow();
runCLI(`lint ${myapp} --cache --cache-location="my-cache"`, {
silenceError: true,
});
expect(() => checkFilesExist(`my-cache/${myapp}`)).not.toThrow();
expect(readCacheFile(`my-cache/${myapp}`)).toContain(
path.normalize(`${myapp}/src/app/app.spec.tsx`)
);
});
it('linting should generate an output file with a specific format', () => {
const eslintrc = readJson('.eslintrc.json');
eslintrc.overrides.forEach((override) => {
if (override.files.includes('*.ts')) {
override.rules['no-console'] = 'error';
}
});
updateFile('.eslintrc.json', JSON.stringify(eslintrc, null, 2));
const outputFile = 'a/b/c/lint-output.json';
expect(() => {
checkFilesExist(outputFile);
}).toThrow();
const stdout = runCLI(
`lint ${myapp} --output-file="${outputFile}" --format=json`,
{
silenceError: true,
}
);
expect(stdout).not.toContain('Unexpected console statement');
expect(() => checkFilesExist(outputFile)).not.toThrow();
const outputContents = JSON.parse(readFile(outputFile));
const outputForApp: any = Object.values(outputContents).filter(
(result: any) =>
result.filePath.includes(path.normalize(`${myapp}/src/main.ts`))
)[0];
expect(outputForApp.errorCount).toBe(1);
expect(outputForApp.messages[0].ruleId).toBe('no-console');
expect(outputForApp.messages[0].message).toBe(
'Unexpected console statement.'
);
}, 1000000);
it('should support creating, testing and using workspace lint rules', () => {
const messageId = 'e2eMessageId';
const libMethodName = 'getMessageId';
// add custom function
updateFile(
`libs/${mylib}/src/lib/${mylib}.ts`,
`export const ${libMethodName} = (): '${messageId}' => '${messageId}';`
);
// Generate a new rule (should also scaffold the required workspace project and tests)
const newRuleName = 'e2e-test-rule-name';
runCLI(`generate @nx/linter:workspace-rule ${newRuleName}`);
// Ensure that the unit tests for the new rule are runnable
const unitTestsOutput = runCLI(`test eslint-rules`);
expect(unitTestsOutput).toContain('Successfully ran target test');
// Update the rule for the e2e test so that we can assert that it produces the expected lint failure when used
const knownLintErrorMessage = 'e2e test known error message';
const newRulePath = `tools/eslint-rules/rules/${newRuleName}.ts`;
const newRuleGeneratedContents = readFile(newRulePath);
const updatedRuleContents = updateGeneratedRuleImplementation(
newRulePath,
newRuleGeneratedContents,
knownLintErrorMessage,
messageId,
libMethodName,
`@${projScope}/${mylib}`
);
updateFile(newRulePath, updatedRuleContents);
const newRuleNameForUsage = `@nx/workspace/${newRuleName}`;
// Add the new workspace rule to the lint config and run linting
const eslintrc = readJson('.eslintrc.json');
eslintrc.overrides.forEach((override) => {
if (override.files.includes('*.ts')) {
override.rules[newRuleNameForUsage] = 'error';
}
});
updateFile('.eslintrc.json', JSON.stringify(eslintrc, null, 2));
const lintOutput = runCLI(`lint ${myapp} --verbose`, {
silenceError: true,
});
expect(lintOutput).toContain(newRuleNameForUsage);
expect(lintOutput).toContain(knownLintErrorMessage);
}, 1000000);
it('lint plugin should ensure module boundaries', () => {
const myapp2 = uniq('myapp2');
const lazylib = uniq('lazylib');
const invalidtaglib = uniq('invalidtaglib');
const validtaglib = uniq('validtaglib');
runCLI(`generate @nx/react:app ${myapp2}`);
runCLI(`generate @nx/react:lib ${lazylib}`);
runCLI(`generate @nx/js:lib ${invalidtaglib} --tags=invalidtag`);
runCLI(`generate @nx/js:lib ${validtaglib} --tags=validtag`);
const eslint = readJson('.eslintrc.json');
eslint.overrides[0].rules[
'@nx/enforce-module-boundaries'
][1].depConstraints = [
{ sourceTag: 'validtag', onlyDependOnLibsWithTags: ['validtag'] },
...eslint.overrides[0].rules['@nx/enforce-module-boundaries'][1]
.depConstraints,
];
updateFile('.eslintrc.json', JSON.stringify(eslint, null, 2));
const tsConfig = readJson('tsconfig.base.json');
/**
* apps do not add themselves to the tsconfig file.
*
* Let's add it so that we can trigger the lint failure
*/
tsConfig.compilerOptions.paths[`@${projScope}/${myapp2}`] = [
`apps/${myapp2}/src/main.ts`,
];
tsConfig.compilerOptions.paths[`@secondScope/${lazylib}`] =
tsConfig.compilerOptions.paths[`@${projScope}/${lazylib}`];
delete tsConfig.compilerOptions.paths[`@${projScope}/${lazylib}`];
updateFile('tsconfig.base.json', JSON.stringify(tsConfig, null, 2));
updateFile(
`apps/${myapp}/src/main.ts`,
`
import '../../../libs/${mylib}';
import '@secondScope/${lazylib}';
import '@${projScope}/${myapp2}';
import '@${projScope}/${invalidtaglib}';
import '@${projScope}/${validtaglib}';
const s = {loadChildren: '@secondScope/${lazylib}'};
`
);
const out = runCLI(`lint ${myapp}`, { silenceError: true });
expect(out).toContain(
'Projects cannot be imported by a relative or absolute path, and must begin with a npm scope'
);
expect(out).toContain('Imports of apps are forbidden');
expect(out).toContain(
'A project tagged with "validtag" can only depend on libs tagged with "validtag"'
);
}, 1000000);
it('should print the effective configuration for a file specified using --printConfig', () => {
const eslint = readJson('.eslintrc.json');
eslint.overrides.push({
files: ['src/index.ts'],
rules: {
'specific-rule': 'off',
},
});
updateFile('.eslintrc.json', JSON.stringify(eslint, null, 2));
const out = runCLI(`lint ${myapp} --printConfig src/index.ts`, {
silenceError: true,
});
expect(out).toContain('"specific-rule": [');
}, 1000000);
});
describe('workspace boundary rules', () => {
const libA = uniq('tslib-a');
const libB = uniq('tslib-b');
const libC = uniq('tslib-c');
beforeAll(() => {
runCLI(`generate @nx/js:lib ${libA}`);
runCLI(`generate @nx/js:lib ${libB}`);
runCLI(`generate @nx/js:lib ${libC}`);
/**
* create tslib-a structure
*/
createFile(
`libs/${libA}/src/lib/tslib-a.ts`,
`
export function libASayHi(): string {
return 'hi there';
}
export function libASayHello(): string {
return 'Hi from tslib-a';
}
`
);
createFile(
`libs/${libA}/src/lib/some-non-exported-function.ts`,
`
export function someNonPublicLibFunction() {
return 'this function is exported, but not via the libs barrel file';
}
export function someSelectivelyExportedFn() {
return 'this fn is exported selectively in the barrel file';
}
`
);
createFile(
`libs/${libA}/src/index.ts`,
`
export * from './lib/tslib-a';
export { someSelectivelyExportedFn } from './lib/some-non-exported-function';
`
);
/**
* create tslib-b structure
*/
createFile(
`libs/${libB}/src/index.ts`,
`
export * from './lib/tslib-b';
`
);
createFile(
`libs/${libB}/src/lib/tslib-b.ts`,
`
import { libASayHi } from 'libs/${libA}/src/lib/tslib-a';
import { libASayHello } from '../../../${libA}/src/lib/tslib-a';
// import { someNonPublicLibFunction } from '../../../${libA}/src/lib/some-non-exported-function';
import { someSelectivelyExportedFn } from '../../../${libA}/src/lib/some-non-exported-function';
export function tslibB(): string {
// someNonPublicLibFunction();
someSelectivelyExportedFn();
libASayHi();
libASayHello();
return 'hi there';
}
`
);
/**
* create tslib-c structure
*/
createFile(
`libs/${libC}/src/index.ts`,
`
export * from './lib/tslib-c';
export * from './lib/constant';
`
);
createFile(
`libs/${libC}/src/lib/constant.ts`,
`
export const SOME_CONSTANT = 'some constant value';
export const someFunc1 = () => 'hi';
export function someFunc2() {
return 'hi2';
}
`
);
createFile(
`libs/${libC}/src/lib/tslib-c-another.ts`,
`
import { tslibC, SOME_CONSTANT, someFunc1, someFunc2 } from '@${projScope}/${libC}';
export function someStuff() {
someFunc1();
someFunc2();
tslibC();
console.log(SOME_CONSTANT);
return 'hi';
}
`
);
createFile(
`libs/${libC}/src/lib/tslib-c.ts`,
`
import { someFunc1, someFunc2, SOME_CONSTANT } from '@${projScope}/${libC}';
export function tslibC(): string {
someFunc1();
someFunc2();
console.log(SOME_CONSTANT);
return 'tslib-c';
}
`
);
});
it('should fix noSelfCircularDependencies', () => {
const stdout = runCLI(`lint ${libC}`, {
silenceError: true,
});
expect(stdout).toContain(
'Projects should use relative imports to import from other files within the same project'
);
// fix them
const fixedStout = runCLI(`lint ${libC} --fix`, {
silenceError: true,
});
expect(fixedStout).toContain(
`Successfully ran target lint for project ${libC}`
);
const fileContent = readFile(`libs/${libC}/src/lib/tslib-c-another.ts`);
expect(fileContent).toContain(`import { tslibC } from './tslib-c';`);
expect(fileContent).toContain(
`import { SOME_CONSTANT, someFunc1, someFunc2 } from './constant';`
);
const fileContentTslibC = readFile(`libs/${libC}/src/lib/tslib-c.ts`);
expect(fileContentTslibC).toContain(
`import { someFunc1, someFunc2, SOME_CONSTANT } from './constant';`
);
});
it('should fix noRelativeOrAbsoluteImportsAcrossLibraries', () => {
const stdout = runCLI(`lint ${libB}`, {
silenceError: true,
});
expect(stdout).toContain(
'Projects cannot be imported by a relative or absolute path, and must begin with a npm scope'
);
// fix them
const fixedStout = runCLI(`lint ${libB} --fix`, {
silenceError: true,
});
expect(fixedStout).toContain(
`Successfully ran target lint for project ${libB}`
);
const fileContent = readFile(`libs/${libB}/src/lib/tslib-b.ts`);
expect(fileContent).toContain(
`import { libASayHello } from '@${projScope}/${libA}';`
);
expect(fileContent).toContain(
`import { libASayHi } from '@${projScope}/${libA}';`
);
expect(fileContent).toContain(
`import { someSelectivelyExportedFn } from '@${projScope}/${libA}';`
);
});
});
describe('dependency checks', () => {
beforeAll(() => {
updateJson(`libs/${mylib}/.eslintrc.json`, (json) => {
json.overrides = [
...json.overrides,
{
files: ['*.json'],
parser: 'jsonc-eslint-parser',
rules: {
'@nx/dependency-checks': 'error',
},
},
];
return json;
});
updateJson(`libs/${mylib}/project.json`, (json) => {
json.targets.lint.options.lintFilePatterns = [
`libs/${mylib}/**/*.ts`,
`libs/${mylib}/project.json`,
`libs/${mylib}/package.json`,
];
return json;
});
});
it('should report dependency check issues', () => {
const rootPackageJson = readJson('package.json');
const nxVersion = rootPackageJson.devDependencies.nx;
const tslibVersion = rootPackageJson.dependencies['tslib'];
let out = runCLI(`lint ${mylib}`, { silenceError: true });
expect(out).toContain('All files pass linting');
// make an explict dependency to nx
updateFile(
`libs/${mylib}/src/lib/${mylib}.ts`,
(content) =>
`import { names } from '@nx/devkit';\n\n` +
content.replace(/return .*;/, `return names(${mylib}).className;`)
);
// output should now report missing dependency
out = runCLI(`lint ${mylib}`, { silenceError: true });
expect(out).toContain('they are missing');
expect(out).toContain('@nx/devkit');
// should fix the missing dependency issue
out = runCLI(`lint ${mylib} --fix`, { silenceError: true });
expect(out).toContain(
`Successfully ran target lint for project ${mylib}`
);
const packageJson = readJson(`libs/${mylib}/package.json`);
expect(packageJson).toMatchInlineSnapshot(`
{
"dependencies": {
"@nx/devkit": "${nxVersion}",
"tslib": "${tslibVersion}",
},
"main": "./src/index.js",
"name": "@proj/${mylib}",
"type": "commonjs",
"typings": "./src/index.d.ts",
"version": "0.0.1",
}
`);
// intentionally set the invalid version
updateJson(`libs/${mylib}/package.json`, (json) => {
json.dependencies['@nx/devkit'] = '100.0.0';
return json;
});
out = runCLI(`lint ${mylib}`, { silenceError: true });
expect(out).toContain(
'version specifier does not contain the installed version of "@nx/devkit"'
);
// should fix the version mismatch issue
out = runCLI(`lint ${mylib} --fix`, { silenceError: true });
expect(out).toContain(
`Successfully ran target lint for project ${mylib}`
);
});
});
});
describe('Flat config', () => {
const packageManager = getSelectedPackageManager() || 'pnpm';
afterEach(() => cleanupProject());
it('should convert integrated to flat config', () => {
const myapp = uniq('myapp');
const mylib = uniq('mylib');
runCreateWorkspace(myapp, {
preset: 'react-monorepo',
appName: myapp,
style: 'css',
packageManager,
bundler: 'vite',
e2eTestRunner: 'none',
});
runCLI(`generate @nx/js:lib ${mylib}`);
// migrate to flat structure
runCLI(`generate @nx/linter:convert-to-flat-config`);
checkFilesExist(
'eslint.config.js',
`apps/${myapp}/eslint.config.js`,
`libs/${mylib}/eslint.config.js`
);
checkFilesDoNotExist(
'.eslintrc.json',
`apps/${myapp}/.eslintrc.json`,
`libs/${mylib}/.eslintrc.json`
);
const outFlat = runCLI(`affected -t lint`, {
silenceError: true,
});
expect(outFlat).toContain('All files pass linting');
}, 1000000);
it('should convert standalone to flat config', () => {
const myapp = uniq('myapp');
const mylib = uniq('mylib');
runCreateWorkspace(myapp, {
preset: 'react-standalone',
appName: myapp,
style: 'css',
packageManager,
bundler: 'vite',
e2eTestRunner: 'none',
});
runCLI(`generate @nx/js:lib ${mylib}`);
// migrate to flat structure
runCLI(`generate @nx/linter:convert-to-flat-config`);
checkFilesExist(
'eslint.config.js',
`${mylib}/eslint.config.js`,
'eslint.base.config.js'
);
checkFilesDoNotExist(
'.eslintrc.json',
`${mylib}/.eslintrc.json`,
'.eslintrc.base.json'
);
const outFlat = runCLI(`affected -t lint`, {
silenceError: true,
});
expect(outFlat).toContain('All files pass linting');
}, 1000000);
});
describe('Root projects migration', () => {
beforeEach(() => newProject());
afterEach(() => cleanupProject());
function verifySuccessfulStandaloneSetup(myapp: string) {
expect(runCLI(`lint ${myapp}`, { silenceError: true })).toContain(
'All files pass linting'
);
expect(runCLI(`lint e2e`, { silenceError: true })).toContain(
'All files pass linting'
);
expect(() => checkFilesExist(`.eslintrc.base.json`)).toThrow();
const rootEslint = readJson('.eslintrc.json');
const e2eEslint = readJson('e2e/.eslintrc.json');
// should directly refer to nx plugin
expect(rootEslint.plugins).toEqual(['@nx']);
expect(e2eEslint.plugins).toEqual(['@nx']);
}
function verifySuccessfulMigratedSetup(myapp: string, mylib: string) {
expect(runCLI(`lint ${myapp}`, { silenceError: true })).toContain(
'All files pass linting'
);
expect(runCLI(`lint e2e`, { silenceError: true })).toContain(
'All files pass linting'
);
expect(runCLI(`lint ${mylib}`, { silenceError: true })).toContain(
'All files pass linting'
);
expect(() => checkFilesExist(`.eslintrc.base.json`)).not.toThrow();
const rootEslint = readJson('.eslintrc.base.json');
const appEslint = readJson('.eslintrc.json');
const e2eEslint = readJson('e2e/.eslintrc.json');
const libEslint = readJson(`libs/${mylib}/.eslintrc.json`);
// should directly refer to nx plugin
expect(rootEslint.plugins).toEqual(['@nx']);
expect(appEslint.plugins).toBeUndefined();
expect(e2eEslint.plugins).toBeUndefined();
expect(libEslint.plugins).toBeUndefined();
// should extend base
expect(appEslint.extends.slice(-1)).toEqual(['./.eslintrc.base.json']);
expect(e2eEslint.extends.slice(-1)).toEqual(['../.eslintrc.base.json']);
expect(libEslint.extends.slice(-1)).toEqual([
'../../.eslintrc.base.json',
]);
}
it('(React standalone) should set root project config to app and e2e app and migrate when another lib is added', () => {
const myapp = uniq('myapp');
const mylib = uniq('mylib');
runCLI(`generate @nx/react:app ${myapp} --rootProject=true`);
verifySuccessfulStandaloneSetup(myapp);
let appEslint = readJson('.eslintrc.json');
let e2eEslint = readJson('e2e/.eslintrc.json');
// should have plugin extends
expect(appEslint.overrides[0].extends).toBeDefined();
expect(appEslint.overrides[1].extends).toBeDefined();
expect(e2eEslint.overrides[0].extends).toBeDefined();
runCLI(`generate @nx/js:lib ${mylib} --unitTestRunner=jest`);
verifySuccessfulMigratedSetup(myapp, mylib);
appEslint = readJson(`.eslintrc.json`);
e2eEslint = readJson('e2e/.eslintrc.json');
// should have no plugin extends
expect(appEslint.overrides[0].extends).toBeUndefined();
expect(appEslint.overrides[1].extends).toBeUndefined();
expect(e2eEslint.overrides[0].extends).toBeUndefined();
});
it('(Angular standalone) should set root project config to app and e2e app and migrate when another lib is added', () => {
const myapp = uniq('myapp');
const mylib = uniq('mylib');
runCLI(
`generate @nx/angular:app ${myapp} --rootProject=true --no-interactive`
);
verifySuccessfulStandaloneSetup(myapp);
let appEslint = readJson('.eslintrc.json');
let e2eEslint = readJson('e2e/.eslintrc.json');
// should have plugin extends
expect(appEslint.overrides[0].extends).toBeDefined();
expect(appEslint.overrides[1].extends).toBeDefined();
expect(e2eEslint.overrides[0].extends).toBeDefined();
runCLI(`generate @nx/js:lib ${mylib} --no-interactive`);
verifySuccessfulMigratedSetup(myapp, mylib);
appEslint = readJson(`.eslintrc.json`);
e2eEslint = readJson('e2e/.eslintrc.json');
// should have no plugin extends
expect(appEslint.overrides[0].extends).toEqual([
'plugin:@nx/angular',
'plugin:@angular-eslint/template/process-inline-templates',
]);
expect(e2eEslint.overrides[0].extends).toBeUndefined();
});
it('(Node standalone) should set root project config to app and e2e app and migrate when another lib is added', () => {
const myapp = uniq('myapp');
const mylib = uniq('mylib');
runCLI(
`generate @nx/node:app ${myapp} --rootProject=true --no-interactive`
);
verifySuccessfulStandaloneSetup(myapp);
let appEslint = readJson('.eslintrc.json');
let e2eEslint = readJson('e2e/.eslintrc.json');
// should have plugin extends
expect(appEslint.overrides[0].extends).toBeDefined();
expect(appEslint.overrides[1].extends).toBeDefined();
expect(e2eEslint.overrides[0].extends).toBeDefined();
runCLI(`generate @nx/js:lib ${mylib} --no-interactive`);
verifySuccessfulMigratedSetup(myapp, mylib);
appEslint = readJson(`.eslintrc.json`);
e2eEslint = readJson('e2e/.eslintrc.json');
// should have no plugin extends
expect(appEslint.overrides[0].extends).toBeUndefined();
expect(appEslint.overrides[1].extends).toBeUndefined();
expect(e2eEslint.overrides[0].extends).toBeUndefined();
});
});
});
/**
* Update the generated rule implementation to produce a known lint error from all files.
*
* It is important that we do this surgically via AST transformations, otherwise we will
* drift further and further away from the original generated code and therefore make our
* e2e test less accurate and less valuable.
*/
function updateGeneratedRuleImplementation(
newRulePath: string,
newRuleGeneratedContents: string,
knownLintErrorMessage: string,
messageId,
libMethodName: string,
libPath: string
): string {
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
const newRuleSourceFile = ts.createSourceFile(
newRulePath,
newRuleGeneratedContents,
ts.ScriptTarget.Latest,
true
);
const transformer = <T extends ts.SourceFile>(
context: ts.TransformationContext
) =>
((rootNode: T) => {
function visit(node: ts.Node): ts.Node {
/**
* Add an ESLint messageId which will show the knownLintErrorMessage
*
* i.e.
*
* messages: {
* e2eMessageId: knownLintErrorMessage
* }
*/
if (
ts.isPropertyAssignment(node) &&
ts.isIdentifier(node.name) &&
node.name.escapedText === 'messages'
) {
return ts.factory.updatePropertyAssignment(
node,
node.name,
ts.factory.createObjectLiteralExpression([
ts.factory.createPropertyAssignment(
messageId,
ts.factory.createStringLiteral(knownLintErrorMessage)
),
])
);
}
/**
* Update the rule implementation to report the knownLintErrorMessage on every Program node
*
* During the debugging of the switch from ts-node to swc-node we found out
* that regular rules would work even without explicit path mapping registration,
* but rules that import runtime functionality from within the workspace
* would break the rule registration.
*
* Instead of having a static literal messageId we retreieved it via imported getMessageId method.
*
* i.e.
*
* create(context) {
* return {
* Program(node) {
* context.report({
* messageId: getMessageId(),
* node,
* });
* }
* }
* }
*/
if (
ts.isMethodDeclaration(node) &&
ts.isIdentifier(node.name) &&
node.name.escapedText === 'create'
) {
return ts.factory.updateMethodDeclaration(
node,
getModifiers(node),
node.asteriskToken,
node.name,
node.questionToken,
node.typeParameters,
node.parameters,
node.type,
ts.factory.createBlock([
ts.factory.createReturnStatement(
ts.factory.createObjectLiteralExpression([
ts.factory.createMethodDeclaration(
[],
undefined,
'Program',
undefined,
[],
[
ts.factory.createParameterDeclaration(
[],
undefined,
'node',
undefined,
undefined,
undefined
),
],
undefined,
ts.factory.createBlock([
ts.factory.createExpressionStatement(
ts.factory.createCallExpression(
ts.factory.createPropertyAccessExpression(
ts.factory.createIdentifier('context'),
'report'
),
[],
[
ts.factory.createObjectLiteralExpression([
ts.factory.createPropertyAssignment(
'messageId',
ts.factory.createCallExpression(
ts.factory.createIdentifier(libMethodName),
[],
[]
)
),
ts.factory.createShorthandPropertyAssignment(
'node'
),
]),
]
)
),
])
),
])
),
])
);
}
return ts.visitEachChild(node, visit, context);
}
/**
* Add lib import as a first line of the rule file.
* Needed for the access of getMessageId in the context report above.
*
* i.e.
*
* import { getMessageId } from "@myproj/mylib";
*
*/
const importAdded = ts.factory.updateSourceFile(rootNode, [
ts.factory.createImportDeclaration(
undefined,
ts.factory.createImportClause(
false,
undefined,
ts.factory.createNamedImports([
ts.factory.createImportSpecifier(
false,
undefined,
ts.factory.createIdentifier(libMethodName)
),
])
),
ts.factory.createStringLiteral(libPath)
),
...rootNode.statements,
]);
return ts.visitNode(importAdded, visit);
}) as ts.Transformer<T>;
const result: ts.TransformationResult<ts.SourceFile> =
ts.transform<ts.SourceFile>(newRuleSourceFile, [transformer]);
const updatedSourceFile: ts.SourceFile = result.transformed[0];
return printer.printFile(updatedSourceFile);
}