blob: 2b5e75f3d2b3fa76dfea178f7e6ff5c22d47f5a9 [file] [log] [blame]
// Copyright 2025 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @file Rule to identify Lit render calls that are not inside of a
* view function.
*/
import type {TSESTree} from '@typescript-eslint/utils';
import {isCommaToken} from '@typescript-eslint/utils/ast-utils';
import {nullThrows, NullThrowsReasons} from '@typescript-eslint/utils/eslint-utils';
import type {ArrowFunctionExpression, FunctionDeclaration, FunctionExpression, Identifier} from 'estree';
import {isLitHtmlRenderCall, isViewFunction} from './utils/lit.ts';
import {createRule} from './utils/ruleCreator.ts';
export default createRule({
name: 'no-lit-render-outside-of-view',
meta: {
type: 'problem',
docs: {
description: 'Lit render calls should be inside of a view function',
category: 'Possible Errors',
},
messages: {
litRenderShouldBeInsideOfView: 'Lit render calls should be inside of a view function',
litRenderInsideOfViewMustUseTarget: 'Lit render calls inside of a view function must use the `target` parameter',
litRenderInsideOfViewMustNotUseHost: 'Lit render calls inside of a view function must not use the `host` option',
},
fixable: 'code',
schema: [], // no options
},
defaultOptions: [],
create: function(context) {
const {sourceCode} = context;
return {
CallExpression(node) {
if (!isLitHtmlRenderCall(node)) {
return;
}
let functionNode: TSESTree.Node|undefined = node.parent;
while (functionNode &&
!['FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression'].includes(functionNode.type)) {
functionNode = functionNode.parent;
}
if (!functionNode || !isViewFunction(functionNode)) {
context.report({
node,
messageId: 'litRenderShouldBeInsideOfView',
});
return;
}
type FunctionLike = FunctionDeclaration|FunctionExpression|ArrowFunctionExpression;
const targetParameterName = ((functionNode as FunctionLike).params[2] as Identifier).name;
const targetArgument = node.arguments[1];
if (targetArgument.type !== 'Identifier' || targetArgument.name !== targetParameterName) {
context.report({
node,
messageId: 'litRenderInsideOfViewMustUseTarget',
fix(fixer) {
return fixer.replaceText(targetArgument, targetParameterName);
}
});
return;
}
if (node.arguments.length < 3) {
return;
}
const renderOptions = node.arguments[2];
if (renderOptions.type !== 'ObjectExpression') {
// Invalid, but TypeScript will catch it.
return;
}
for (const renderOption of renderOptions.properties) {
if (renderOption.type === 'Property' && renderOption.key.type === 'Identifier' &&
renderOption.key.name === 'host') {
context.report({
node,
messageId: 'litRenderInsideOfViewMustNotUseHost',
fix(fixer) {
if (renderOptions.properties.length === 1) {
const commaToken = nullThrows(
sourceCode.getTokenBefore(renderOptions, isCommaToken),
NullThrowsReasons.MissingToken(',', node.type));
return fixer.removeRange([commaToken.range[0], renderOptions.range[1]]);
}
const prevToken = sourceCode.getTokenBefore(renderOption);
if (prevToken && isCommaToken(prevToken)) {
return fixer.removeRange([prevToken.range[0], renderOption.range[1]]);
}
const nextToken = sourceCode.getTokenAfter(renderOption);
if (nextToken && isCommaToken(nextToken)) {
return fixer.removeRange([renderOption.range[0], nextToken.range[1]]);
}
return [];
}
});
}
}
},
};
}
});