[eslint] Run type checking on custom rules

Currently behind a flag, as there are a lot of error.
This CL fixes some of them.

Bug: 407085691
Change-Id: I736472ff5a9d8c46e45a1ce89a42199774d9075f
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6410471
Auto-Submit: Nikolay Vitkov <nvitkov@chromium.org>
Reviewed-by: Danil Somsikov <dsv@chromium.org>
Commit-Queue: Nikolay Vitkov <nvitkov@chromium.org>
This commit is contained in:
Nikolay Vitkov
2025-03-28 16:08:54 -07:00
committed by Devtools-frontend LUCI CQ
parent 1bbd38cc8d
commit 4eac8f8231
27 changed files with 319 additions and 145 deletions
+1
View File
@@ -51,3 +51,4 @@ test/perf/.generated
# Linters caches
.eslintcache
.stylelintcache
**/tsconfig.tsbuildinfo
+1 -1
View File
@@ -33,7 +33,7 @@ module.exports = {
const filename = context.filename ?? context.getFilename();
return {
ImportDeclaration(node) {
const importPath = path.normalize(node.source.value);
const importPath = path.normalize(`${node.source.value}`);
if (importPath.endsWith('.css.js')) {
const importingFileName = path.resolve(filename);
@@ -19,7 +19,7 @@ const FRONT_END_FOLDER = path.join(
'front_end',
);
const CURRENT_YEAR = new Date().getFullYear();
const CURRENT_YEAR = `${new Date().getFullYear()}`;
const LINE_LICENSE_HEADER = [
`Copyright ${CURRENT_YEAR} The Chromium Authors. All rights reserved.`,
'Use of this source code is governed by a BSD-style license that can be',
@@ -51,12 +51,16 @@ module.exports = {
const sourceCode = context.sourceCode ?? context.getSourceCode();
return {
MemberExpression(node) {
if (node.object.type !== 'Identifier' || node.property.type !== 'Identifier') {
return;
}
if ((node.object.name === 'it' || node.object.name === 'describe' || node.object.name === 'itScreenshot') &&
(node.property.name === 'skip' || node.property.name === 'skipOnPlatforms') &&
node.parent.type === 'CallExpression') {
const testNameNode = node.property.name === 'skip' ? node.parent.arguments[0] : node.parent.arguments[1];
if(!testNameNode) {
if (!testNameNode) {
return;
}
@@ -78,7 +82,7 @@ module.exports = {
},
CallExpression(node) {
if (node.callee.name === 'it' && node.arguments[0]) {
if (node.callee.type === 'Identifier' && node.callee.name === 'it' && node.arguments[0]) {
const textValue = getTextValue(node.arguments[0]);
if (textValue && TEST_NAME_REGEX.test(textValue)) {
@@ -21,13 +21,40 @@ module.exports = {
create: function(context) {
return {
MethodDefinition(node) {
if (node.key.type !== 'Identifier') {
return;
}
const nodeName = node.key.name;
if (node.parent.parent.superClass?.property?.name === 'Widget' && nodeName === 'wasShown') {
if (nodeName !== 'wasShown') {
return;
}
const ancestorClass = node.parent.parent;
if (ancestorClass.type !== 'ClassDeclaration') {
return;
}
if (
ancestorClass.superClass?.type === 'MemberExpression' &&
ancestorClass.superClass.property.type === 'Identifier' &&
ancestorClass.superClass.property.name === 'Widget'
) {
const topBodyNode = node.value.body.body[0];
if (!(topBodyNode.type === 'ExpressionStatement' && topBodyNode.expression.type === 'CallExpression' &&
topBodyNode.expression.callee.object.type === 'Super' &&
topBodyNode.expression.callee.property.name === 'wasShown')) {
context.report({node, message: 'Please make sure the first call in wasShown is to super.wasShown().'});
if (
!(
topBodyNode.type === 'ExpressionStatement' &&
topBodyNode.expression.type === 'CallExpression' &&
topBodyNode.expression.callee.type === 'MemberExpression' &&
topBodyNode.expression.callee.object.type === 'Super' &&
topBodyNode.expression.callee.property.type === 'Identifier' &&
topBodyNode.expression.callee.property.name === 'wasShown'
)
) {
context.report({
node,
message:
'Please make sure the first call in wasShown is to super.wasShown().',
});
}
}
}
@@ -7,7 +7,7 @@
const path = require('path');
function isStarAsImportSpecifier(specifiers) {
return specifiers.length === 1 && specifiers[0].type === 'ImportNamespaceSpecifier';
return (specifiers.length === 1 && specifiers[0].type === 'ImportNamespaceSpecifier');
}
/**
@@ -49,8 +49,9 @@ module.exports = {
// conventions for module imports.
return;
}
const importPath = path.normalize(node.source.value);
const importPathForErrorMessage = node.source.value.replace(/\\/g, '/');
const value = `${node.source.value}`;
const importPath = path.normalize(value);
const importPathForErrorMessage = value.replace(/\\/g, '/');
const absoluteImportPath = path.resolve(importingDir, importPath);
const importNameInCode = node.specifiers[0].local.name;
@@ -58,8 +58,15 @@ module.exports = {
node: declaration,
messageId: 'invalidUIStringsObject',
fix: fixer => {
const objectEnd = declaration.init.range[1];
return fixer.insertTextAfterRange([objectEnd - 1, objectEnd], ' as const');
const objectEnd = declaration.init?.range?.[1];
if (!objectEnd) {
return null;
}
return fixer.insertTextAfterRange(
[objectEnd - 1, objectEnd],
' as const',
);
},
});
},
+13 -11
View File
@@ -175,9 +175,9 @@ module.exports = {
if (!node.source) {
return;
}
const importPath = path.normalize(node.source.value);
const importPathForErrorMessage = node.source.value.replace(/\\/g, '/');
const value = `${node.source.value}`;
const importPath = path.normalize(value);
const importPathForErrorMessage = value.replace(/\\/g, '/');
checkImportExtension(
importPath,
importPathForErrorMessage,
@@ -186,12 +186,13 @@ module.exports = {
);
},
ImportDeclaration(node) {
if (node.source.value.includes('//')) {
const value = `${node.source.value}`;
if (value.includes('//')) {
context.report({
node,
messageId: 'doubleSlashInImportPath',
fix(fixer) {
const fixedValue = node.source.value.replaceAll('//', '/');
const fixedValue = value.replaceAll('//', '/');
// Replace the original import string with the fixed one. We need
// the extra quotes around the value to ensure we produce valid
// JS - else it would end up as `import X from ../some/path.js`
@@ -199,8 +200,9 @@ module.exports = {
},
});
}
const importPath = path.normalize(node.source.value);
const importPathForErrorMessage = node.source.value.replace(/\\/g, '/');
const importPath = path.normalize(value);
const importPathForErrorMessage = value.replace(/\\/g, '/');
checkImportExtension(
node.source.value,
@@ -210,12 +212,15 @@ module.exports = {
);
// Type imports are unrestricted
// @ts-expect-error needs typescript
if (node.importKind === 'type') {
// `import type ... from ...` syntax
return;
}
// @ts-expect-error needs typescript
if (node.importKind === 'value') {
// `import {type ...} from ...` syntax
// @ts-expect-error needs typescript
if (node.specifiers.every(spec => spec.importKind === 'type')) {
return;
}
@@ -228,10 +233,7 @@ module.exports = {
//
// Don't use `importPath` here, as `path.normalize` removes
// the `./` from same-folder import paths.
if (
!node.source.value.startsWith('.') &&
!/^[\w\-_]+$/.test(node.source.value)
) {
if (!value.startsWith('.') && !/^[\w\-_]+$/.test(value)) {
context.report({
node,
message:
@@ -62,7 +62,7 @@ module.exports = {
// Get the absolute path of the current file's directory, so we can
// compare it to COMMON_INPUT_STYLES and see if the file does import the common styles.
const absoluteDirectory = path.dirname(path.resolve(filename));
const fullImportPath = path.resolve(absoluteDirectory, node.source.value);
const fullImportPath = path.resolve(absoluteDirectory, `${node.source.value}`);
foundInputStylesImport = fullImportPath === COMMON_INPUT_STYLES;
if (foundInputStylesImport) {
inputStylesImportedName = node.specifiers[0].local.name;
@@ -90,8 +90,8 @@ module.exports = {
const previousFileLocationArgument = callExpression.arguments[0];
const actualPath = path.join(
frontEndDirectory,
previousFileLocationArgument.value,
frontEndDirectory,
`${previousFileLocationArgument.value}`,
);
if (!allowedPathArguments.includes(actualPath)) {
const newFileName = currentFileRelativeToFrontEnd.replace(/\\/g, '/');
+39 -30
View File
@@ -30,38 +30,47 @@ module.exports = {
return {
CallExpression(node) {
if (
node.callee.type === 'MemberExpression' &&
node.callee.object.name === 'assert' &&
node.callee.property.name === 'equal'
node.callee.type !== 'MemberExpression' ||
node.callee.object.type !== 'Identifier' ||
node.callee.object.name !== 'assert' ||
node.callee.property.type !== 'Identifier' ||
node.callee.property.name !== 'equal'
) {
context.report({
node,
message:
'assert.equal is non-strict. Use assert.strictEqual or assert.deepEqual to compare objects',
fix(fixer) {
/**
* Get the type of the second argument and try to match it to a assert type
*/
const compareToType = node.arguments.at(1)?.type;
if (
// Match number or string
compareToType === 'Literal' ||
// Match `` string
compareToType === 'TemplateElement'
) {
return fixer.replaceText(node.callee.property, 'strictEqual');
}
if (
// Match any object `{...}`
compareToType === 'ObjectExpression' ||
// Match any array `[...]`
compareToType === 'ArrayExpression'
) {
return fixer.replaceText(node.callee.property, 'deepEqual');
}
},
});
return;
}
const calleeProperty = node.callee.property;
context.report({
node,
message:
'assert.equal is non-strict. Use assert.strictEqual or assert.deepEqual to compare objects',
fix(fixer) {
/**
* Get the type of the second argument and try to match it to a assert type
*/
const compareToType = node.arguments.at(1)?.type;
if (
// Match number or string
compareToType === 'Literal' ||
// Match `` string
// @ts-expect-error
compareToType === 'TemplateElement'
) {
return fixer.replaceText(calleeProperty, 'strictEqual');
}
if (
// Match any object `{...}`
compareToType === 'ObjectExpression' ||
// Match any array `[...]`
compareToType === 'ArrayExpression'
) {
return fixer.replaceText(calleeProperty, 'deepEqual');
}
return null;
},
});
},
};
},
@@ -24,18 +24,22 @@ const widget = require('./no-imperative-dom-api/widget.js');
/** @typedef {import('eslint').Scope.Variable} Variable */
/** @typedef {import('eslint').Scope.Reference} Reference*/
/**
* @type {import('eslint').Rule.RuleModule}
*/
module.exports = {
meta : {
type : 'problem',
docs : {
description : 'Prefer template literals over imperative DOM API calls',
category : 'Possible Errors',
meta: {
type: 'problem',
docs: {
description: 'Prefer template literals over imperative DOM API calls',
category: 'Possible Errors',
},
messages: {
preferTemplateLiterals: 'Prefer template literals over imperative DOM API calls',
preferTemplateLiterals:
'Prefer template literals over imperative DOM API calls',
},
fixable : 'code',
schema : [] // no options
fixable: 'code',
schema: [], // no options
},
create : function(context) {
const sourceCode = context.getSourceCode();
@@ -60,7 +64,7 @@ module.exports = {
}
}
if (event.type === 'Literal') {
return event.value.toString();
return event.value?.toString() ?? null;
}
return null;
}
@@ -12,7 +12,7 @@ const {isIdentifier} = require('./ast.js');
/** @typedef {import('./dom-fragment.js').DomFragment} DomFragment */
module.exports = {
create : function(context) {
create: function (context) {
const sourceCode = context.getSourceCode();
return {
@@ -34,7 +34,8 @@ module.exports = {
return true;
}
}
}
return false;
},
};
}
},
};
@@ -74,8 +74,11 @@ module.exports = {
if (isIdentifier(property, 'setAttribute')) {
const attribute = firstArg;
const value = secondArg;
if (attribute.type === 'Literal' && value.type !== 'SpreadElement') {
domFragment.attributes.push({key: attribute.value.toString(), value});
if (attribute.type === 'Literal' && attribute.value && value.type !== 'SpreadElement') {
domFragment.attributes.push({
key: attribute.value.toString(),
value,
});
return true;
}
}
@@ -60,7 +60,9 @@ class DomFragment {
}
}
if (key instanceof ClassMember) {
result.references = [...key.references].map(r => ({node: /** @type {EsLintNode} */ (r)}));
result.references = [...key.references].map(r => ({
node: /** @type {EsLintNode} */ (r),
}));
result.initializer = /** @type {EsLintNode} */ (key.initializer);
}
return result;
@@ -90,7 +92,7 @@ class DomFragment {
return node;
}
if (node.type === 'Literal' && !quoteLiterals) {
return node.value.toString();
return node.value?.toString() ?? '';
}
const text = sourceCode.getText(node);
if (node.type === 'TemplateLiteral') {
@@ -120,16 +122,31 @@ class DomFragment {
lineLength += this.tagName.length + 1;
}
if (this.classList.length) {
appendExpression(`class="${this.classList.map(c => toOutputString(c)).join(' ')}"`);
appendExpression(
`class="${this.classList.map(c => toOutputString(c)).join(' ')}"`,
);
}
for (const attribute of this.attributes || []) {
appendExpression(`${attribute.key}=${attributeValue(toOutputString(attribute.value))}`);
appendExpression(
`${attribute.key}=${attributeValue(toOutputString(attribute.value))}`,
);
}
for (const eventListener of this.eventListeners || []) {
appendExpression(`@${eventListener.key}=${attributeValue(toOutputString(eventListener.value))}`);
appendExpression(
`@${eventListener.key}=${
attributeValue(
toOutputString(eventListener.value),
)}`,
);
}
for (const binding of this.bindings || []) {
appendExpression(`.${binding.key}=${toOutputString(binding.value, /* quoteLiterals=*/ true)}`);
appendExpression(
`.${binding.key}=${
toOutputString(
binding.value,
/* quoteLiterals=*/ true,
)}`,
);
}
if (this.style.length) {
const style = this.style.map(s => `${s.key}:${toOutputString(s.value)}`).join('; ');
@@ -147,7 +164,9 @@ class DomFragment {
}
components.push(`\n${' '.repeat(indent)}`);
}
components.push('</', this.tagName, '>');
if (this.tagName) {
components.push('</', this.tagName, '>');
}
return components;
}
@@ -35,8 +35,8 @@ module.exports = {
const fileNameOfFileBeingChecked = path.resolve(filename);
return {
'ImportDeclaration'(node) {
const importPath = path.resolve(path.dirname(fileNameOfFileBeingChecked), node.source.value);
ImportDeclaration(node) {
const importPath = path.resolve(path.dirname(fileNameOfFileBeingChecked), `${node.source.value}`);
for (const banned of bannedPaths) {
if (importPath.includes(banned)) {
context.report({
@@ -24,7 +24,7 @@ module.exports = {
return {
ClassDeclaration(node) {
// Use `extends LitElement` as a signal.
if (node.superClass?.name !== 'LitElement') {
if (node.superClass?.type !== 'Identifier' || node.superClass?.name !== 'LitElement') {
return;
}
// Existing components are still allowed.
@@ -22,19 +22,20 @@ module.exports = {
return {
ClassDeclaration(node) {
// Only enforce this rule for custom elements
if (!node.superClass || node.superClass.name !== 'HTMLElement') {
if (!node.superClass || node.superClass.type !== 'Identifier' || node.superClass.name !== 'HTMLElement') {
return;
}
const dataSetterDefinition = node.body.body.find(methodDefinition => {
return methodDefinition.kind === 'set' && methodDefinition.key.name === 'data';
return (
'kind' in methodDefinition && methodDefinition.kind === 'set' && methodDefinition.key.name === 'data');
});
if (!dataSetterDefinition) {
if (!dataSetterDefinition || dataSetterDefinition.type === 'StaticBlock') {
return;
}
const dataSetterParam = dataSetterDefinition.value.params[0];
const dataSetterParam = dataSetterDefinition.value?.params?.[0];
if (!dataSetterParam) {
context.report(
{node: dataSetterDefinition, message: 'A data setter must take a parameter that is explicitly typed.'});
@@ -28,13 +28,16 @@ module.exports = {
},
create: function(context) {
function findConstructorAndSuperCallAndFirstArgumentToSuper(node) {
/**
* @type {{constructor: any, superExpression: any, firstArgumentToSuper: any}}
*/
const foundNodes = {
constructor: undefined,
superExpression: undefined,
firstArgumentToSuper: undefined,
};
const constructor = node.body.body.find(bodyNode => {
return bodyNode.type === 'MethodDefinition' && bodyNode.key?.name === 'constructor';
return (bodyNode.type === 'MethodDefinition' && bodyNode.key?.name === 'constructor');
});
if (!constructor) {
return foundNodes;
@@ -165,7 +168,7 @@ module.exports = {
return;
}
if (!node.superClass) {
if (!node.superClass || node.superClass.type !== 'Identifier') {
return;
}
if (node.superClass.name !== 'Event') {
@@ -23,19 +23,23 @@ module.exports = {
},
schema: [] // no options
},
create: function(context) {
const MOCHA_CALLS_TO_CHECK = new Set([
'it',
'before',
'beforeEach',
'after',
'afterEach',
]);
function walkUpTreeToFindMochaFunctionCall(node) {
if(node.type === 'CallExpression' && node.callee.type === 'Identifier' && MOCHA_CALLS_TO_CHECK.has(node.callee.name)) {
create: function (context) {
const MOCHA_CALLS_TO_CHECK = new Set([
'it',
'before',
'beforeEach',
'after',
'afterEach',
]);
function walkUpTreeToFindMochaFunctionCall(node) {
if (
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
MOCHA_CALLS_TO_CHECK.has(node.callee.name)
) {
return node;
}
if(!node || !node.parent) {
if (!node || !node.parent) {
return null;
}
return walkUpTreeToFindMochaFunctionCall(node.parent);
@@ -43,14 +47,18 @@ module.exports = {
return {
MemberExpression(node) {
const objectIsTraceLoader = node.object.type === 'Identifier' && node.object.name === 'TraceLoader';
if(!objectIsTraceLoader) {
const objectIsTraceLoader =
node.object.type === 'Identifier' &&
node.object.name === 'TraceLoader';
if (!objectIsTraceLoader) {
return;
}
// Find out if this is an await call (which needs the additional test timeout).
const callExpression = node.parent;
const isAwait = callExpression.parent && callExpression.parent.type === 'AwaitExpression';
if(!isAwait) {
const isAwait =
callExpression.parent &&
callExpression.parent.type === 'AwaitExpression';
if (!isAwait) {
return;
}
// We now know that we have await TraceLoader.[something]();
@@ -58,24 +66,25 @@ module.exports = {
// we can then check that its function is defined via a function
// and not as an arrow function.
const mochaFunctionCall = walkUpTreeToFindMochaFunctionCall(node);
if(!mochaFunctionCall) {
if (!mochaFunctionCall) {
return;
}
// This code is within a mocha call. If the call is an `it`, we need
// the second argument, otherwise we use the first argument (Mocha
// functions like `beforeEach` take only a function as the argument.)
const functionArg = mochaFunctionCall.callee.name === 'it' ?
mochaFunctionCall.arguments[1] :
mochaFunctionCall.arguments[0];
const functionArg =
mochaFunctionCall.callee.name === 'it'
? mochaFunctionCall.arguments[1]
: mochaFunctionCall.arguments[0];
if(!functionArg) {
if (!functionArg) {
// The node unexpectedly does not have a function passed. The
// developer is probably in the middle of writing it, so we should
// just stop and leave them to it.
return;
}
if(functionArg.type === 'ArrowFunctionExpression') {
if (functionArg.type === 'ArrowFunctionExpression') {
context.report({
node: functionArg,
messageId: 'needsFunction',
@@ -87,11 +96,13 @@ module.exports = {
functionArg.range[0],
functionArg.range[0] + 11,
];
// @ts-expect-error the wrapper function is not typed
return fixer.replaceTextRange(rangeToReplace, 'async function()');
}
},
});
}
}
},
};
}
},
};
@@ -98,7 +98,7 @@ new RuleTester().run('check-test-definitions', rule, {
});
`,
filename: 'test/e2e/folder/file.ts',
errors: [{message: rule.meta.messages.missingBugId}],
errors: [{messageId: 'missingBugId'}],
},
{
code: `import {describe, it} from '../../shared/mocha-extensions.js';
@@ -109,7 +109,7 @@ new RuleTester().run('check-test-definitions', rule, {
});
`,
filename: 'test/e2e/folder/file.ts',
errors: [{message: rule.meta.messages.comment}],
errors: [{messageId: 'comment'}],
},
{
code: `import {describe, it} from '../../shared/mocha-extensions.js';
@@ -121,7 +121,7 @@ new RuleTester().run('check-test-definitions', rule, {
});
`,
filename: 'test/e2e/folder/file.ts',
errors: [{message: rule.meta.messages.missingBugId}],
errors: [{messageId: 'missingBugId'}],
},
{
code: `describe('e2e-test', async () => {
@@ -131,7 +131,7 @@ new RuleTester().run('check-test-definitions', rule, {
});
`,
filename: 'test/e2e/folder/file.ts',
errors: [{message: rule.meta.messages.missingBugId}],
errors: [{messageId: 'missingBugId'}],
},
{
code: `import {describe, it} from '../../shared/mocha-extensions.js';
@@ -142,7 +142,7 @@ new RuleTester().run('check-test-definitions', rule, {
});
`,
filename: 'test/e2e/folder/file.ts',
errors: [{message: rule.meta.messages.extraBugId}],
errors: [{messageId: 'extraBugId'}],
},
{
code: `import {describe, it} from '../../shared/mocha-extensions.js';
@@ -150,10 +150,7 @@ new RuleTester().run('check-test-definitions', rule, {
});
`,
filename: 'test/e2e/folder/file.ts',
errors: [
{message: rule.meta.messages.missingBugId},
{message: rule.meta.messages.comment},
],
errors: [{messageId: 'missingBugId'}, {messageId: 'comment'}],
},
{
code: `import {describe, it} from '../../shared/mocha-extensions.js';
@@ -163,10 +160,7 @@ new RuleTester().run('check-test-definitions', rule, {
});
`,
filename: 'test/e2e/folder/file.ts',
errors: [
{message: rule.meta.messages.missingBugId},
{message: rule.meta.messages.comment},
],
errors: [{messageId: 'missingBugId'}, {messageId: 'comment'}],
},
],
});
@@ -2,11 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
process.env.ESLINT_FAIL_ON_UNKNOWN_JSLOG_CONTEXT_VALUE = 1;
process.env.ESLINT_FAIL_ON_UNKNOWN_JSLOG_CONTEXT_VALUE = 'true';
const rule = require('../lib/jslog-context-list.js');
const {RuleTester} = require('./utils/utils.js');
new RuleTester().run('jslog-context-list', rule, {
invalid: [
{
@@ -29,7 +29,6 @@ new RuleTester().run('no-imports-in-directory', rule, {
],
},
],
errors: [{messageId: 'invalidImport'}],
},
],
invalid: [
+25 -16
View File
@@ -7,33 +7,42 @@ const {assert} = require('chai');
const utils = require('../lib/utils.js');
function getParsedExpression(code) {
const parsed = parser.parse(code).body[0];
if (parsed.type !== 'ExpressionStatement') {
throw new Error('Not an expression');
}
return parsed.expression;
}
describe('eslint utils', () => {
describe('isLitHtmlTemplateCall', () => {
it('returns true if the code is Lit.html``', () => {
const code = 'Lit.html`<span>foo</span>`';
const parsed = parser.parse(code);
const result = utils.isLitHtmlTemplateCall(parsed.body[0].expression);
const expression = getParsedExpression(code);
const result = utils.isLitHtmlTemplateCall(expression);
assert.strictEqual(result, true);
});
it('returns true if the code is html``', () => {
const code = 'html`<span>foo</span>`';
const parsed = parser.parse(code);
const result = utils.isLitHtmlTemplateCall(parsed.body[0].expression);
const expression = getParsedExpression(code);
const result = utils.isLitHtmlTemplateCall(expression);
assert.strictEqual(result, true);
});
it('returns false if the code is Lit.somethingElse``', () => {
const code = 'Lit.somethingElse`<span>foo</span>`';
const parsed = parser.parse(code);
const result = utils.isLitHtmlTemplateCall(parsed.body[0].expression);
const expression = getParsedExpression(code);
const result = utils.isLitHtmlTemplateCall(expression);
assert.strictEqual(result, false);
});
it('returns false if the code is another tagged template function``', () => {
const code = 'notLitHtml`<span>foo</span>`';
const parsed = parser.parse(code);
const result = utils.isLitHtmlTemplateCall(parsed.body[0].expression);
const expression = getParsedExpression(code);
const result = utils.isLitHtmlTemplateCall(expression);
assert.strictEqual(result, false);
});
});
@@ -41,29 +50,29 @@ describe('eslint utils', () => {
describe('isLitHtmlRenderCall', () => {
it('returns true if the code is Lit.render()', () => {
const code = 'Lit.render(Lit.html``, this.#shadow)';
const parsed = parser.parse(code);
const result = utils.isLitHtmlRenderCall(parsed.body[0].expression);
const expression = getParsedExpression(code);
const result = utils.isLitHtmlRenderCall(expression);
assert.strictEqual(result, true);
});
it('returns true if the code is render()', () => {
const code = 'render(html``, this.#shadow)';
const parsed = parser.parse(code);
const result = utils.isLitHtmlRenderCall(parsed.body[0].expression);
const expression = getParsedExpression(code);
const result = utils.isLitHtmlRenderCall(expression);
assert.strictEqual(result, true);
});
it('returns false if the code is not render()', () => {
const code = 'notRender(html``, this.#shadow)';
const parsed = parser.parse(code);
const result = utils.isLitHtmlRenderCall(parsed.body[0].expression);
const expression = getParsedExpression(code);
const result = utils.isLitHtmlRenderCall(expression);
assert.strictEqual(result, false);
});
it('returns false if the code is Lit.notRender()', () => {
const code = 'Lit.notRender(html``, this.#shadow)';
const parsed = parser.parse(code);
const result = utils.isLitHtmlRenderCall(parsed.body[0].expression);
const expression = getParsedExpression(code);
const result = utils.isLitHtmlRenderCall(expression);
assert.strictEqual(result, false);
});
});
+1 -1
View File
@@ -12,7 +12,7 @@ const eslint = require('eslint');
*/
class RuleTester extends eslint.RuleTester {
/**
* @param {import(eslint).Linter.Config} config
* @param {import('eslint').Linter.Config} config
*/
constructor(config = {}) {
super({
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "../../config/typescript/tsconfig.base.json",
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "nodenext",
"lib": ["esnext", "dom"],
"outDir": "ignored",
"checkJs": true,
"noEmit": true,
"noImplicitAny": false
}
}
+68
View File
@@ -15,6 +15,7 @@ import {
devtoolsRootPath,
litAnalyzerExecutablePath,
nodePath,
nodeModulesPath,
tsconfigJsonPath,
} from '../devtools_paths.js';
@@ -30,6 +31,11 @@ const flags = yargs(hideBin(process.argv))
describe:
'Disable cache validations during debugging, useful for custom rule creation/debugging.',
})
.option('tsc', {
type: 'boolean',
default: false,
describe: 'Temperary here while fixing EsLint rule',
})
.usage('$0 [<files...>]', 'Run the linter on the provided files', yargs => {
yargs.positional('files', {
describe: 'File(s), glob(s), or directories',
@@ -268,6 +274,61 @@ function shouldIgnoreFile(path) {
return false;
}
async function runEslintRulesTypeCheck(_files) {
const tscPath = join(nodeModulesPath(), '.bin', 'tsc');
const tsConfigEslintRules = join(
devtoolsRootPath(),
'scripts',
'eslint_rules',
'tsconfig.json',
);
const args = [tscPath, '-b', tsConfigEslintRules];
/**
* @returns {Promise<{output: string, error: string, status:boolean}>}
*/
async function runTypeCheck() {
const result = {
output: '',
error: '',
status: false,
};
return await new Promise(resolve => {
const litAnalyzerProcess = spawn(nodePath(), args, {
encoding: 'utf-8',
cwd: devtoolsRootPath(),
});
litAnalyzerProcess.stdout.on('data', data => {
result.output += `\n${data.toString()}`;
});
litAnalyzerProcess.stderr.on('data', data => {
result.error += `\n${data.toString()}`;
});
litAnalyzerProcess.on('error', message => {
result.error += `\n${message}`;
resolve(result);
});
litAnalyzerProcess.on('exit', code => {
result.status = code === 0;
resolve(result);
});
});
}
const result = await runTypeCheck();
if (result.output && !result.output.includes('Found 0 problems')) {
console.log(result.output);
}
if (result.error) {
console.log(result.error);
}
return result.status;
}
async function run() {
const scripts = [];
const styles = [];
@@ -287,6 +348,9 @@ async function run() {
}
const frontEndFiles = scripts.filter(script => script.includes('front_end'));
const esLintRules = scripts.filter(script =>
script.includes('scripts/eslint_rules'),
);
let succeed = true;
if (scripts.length !== 0) {
@@ -298,6 +362,10 @@ async function run() {
if (styles.length !== 0) {
succeed &&= await runStylelint(styles);
}
if (esLintRules.length !== 0 && flags.tsc) {
succeed &&= await runEslintRulesTypeCheck();
}
return succeed;
}