lwc-linter
Version:
A comprehensive CLI tool for linting Lightning Web Components v8.0.0+ with modern LWC patterns, decorators, lifecycle hooks, and Salesforce platform integration
354 lines • 16.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.loadCodeQualityRules = loadCodeQualityRules;
function loadCodeQualityRules() {
return [
// LWC v8.0.0+ API Versioning Rules
{
name: 'lwc-api-version',
description: 'Ensure LWC components use modern API version (60.0+)',
category: 'code-quality',
severity: 'warn',
fixable: true,
check: (content, filePath, config) => {
const issues = [];
if (filePath.endsWith('.js-meta.xml')) {
const apiVersionMatch = content.match(/<apiVersion>(\d+\.\d+)<\/apiVersion>/);
if (apiVersionMatch) {
const version = parseFloat(apiVersionMatch[1]);
if (version < 60.0) {
issues.push({
rule: 'lwc-api-version',
message: `Consider upgrading to API version 60.0+ for LWC v8.0.0+ features. Current: ${version}`,
severity: 'warn',
line: content.substring(0, apiVersionMatch.index).split('\n').length,
fixable: true,
category: 'code-quality'
});
}
}
}
return issues;
},
fix: (content, issues) => {
let fixedContent = content;
issues.forEach(issue => {
if (issue.rule === 'lwc-api-version') {
fixedContent = fixedContent.replace(/<apiVersion>\d+\.\d+<\/apiVersion>/, '<apiVersion>60.0</apiVersion>');
}
});
return fixedContent;
}
},
// Lightning Web Security Compliance
{
name: 'lws-compliance',
description: 'Check for Lightning Web Security (LWS) best practices',
category: 'code-quality',
severity: 'warn',
fixable: false,
check: (content, filePath, config) => {
const issues = [];
const lines = content.split('\n');
lines.forEach((line, index) => {
// Check for unsafe DOM manipulation that works better with LWS
if (line.includes('document.querySelector') && !line.includes('this.template.querySelector')) {
issues.push({
rule: 'lws-compliance',
message: 'Consider using this.template.querySelector() for better LWS compatibility',
severity: 'warn',
line: index + 1,
fixable: false,
category: 'code-quality'
});
}
// Check for Lightning Locker patterns that can be modernized with LWS
if (line.includes('$A.') && filePath.endsWith('.js')) {
issues.push({
rule: 'lws-compliance',
message: 'Consider migrating from Aura ($A) patterns to modern LWC with Lightning Web Security',
severity: 'warn',
line: index + 1,
fixable: false,
category: 'code-quality'
});
}
});
return issues;
},
fix: (content, issues) => content
},
// Modern LWC Lifecycle Hooks
{
name: 'modern-lifecycle-hooks',
description: 'Use modern LWC v8.0.0+ lifecycle patterns',
category: 'code-quality',
severity: 'info',
fixable: true,
check: (content, filePath, config) => {
const issues = [];
const lines = content.split('\n');
lines.forEach((line, index) => {
// Suggest modern async patterns for connectedCallback
if (line.includes('connectedCallback()') && !line.includes('async')) {
issues.push({
rule: 'modern-lifecycle-hooks',
message: 'Consider using async connectedCallback() for modern LWC patterns',
severity: 'info',
line: index + 1,
fixable: true,
category: 'code-quality'
});
}
// Check for deprecated @track usage (Spring '20+)
if (line.includes('@track') && !line.includes('object') && !line.includes('array')) {
issues.push({
rule: 'modern-lifecycle-hooks',
message: '@track is only needed for objects/arrays in modern LWC. Primitive fields are reactive by default.',
severity: 'info',
line: index + 1,
fixable: true,
category: 'code-quality'
});
}
});
return issues;
},
fix: (content, issues) => {
let fixedContent = content;
// Remove unnecessary @track from primitive fields
fixedContent = fixedContent.replace(/@track\s+((?!.*[{}[\]]).+;)/g, '$1');
return fixedContent;
}
},
{
name: 'no-unused-vars',
description: 'Disallow unused variables in LWC components',
category: 'code-quality',
severity: 'warn',
fixable: true,
check: (content, filePath, config) => {
const issues = [];
const lines = content.split('\n');
// Simple regex to find variable declarations
const variableRegex = /(?:let|const|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
const usageRegex = (varName) => new RegExp(`\\b${varName}\\b`, 'g');
let match;
while ((match = variableRegex.exec(content)) !== null) {
const varName = match[1];
const usageMatches = content.match(usageRegex(varName));
// If variable is only declared but not used (appears only once)
if (usageMatches && usageMatches.length === 1) {
const lineNumber = content.substring(0, match.index).split('\n').length;
issues.push({
rule: 'no-unused-vars',
message: `Unused variable '${varName}'`,
severity: 'warn',
line: lineNumber,
fixable: true,
category: 'code-quality'
});
}
}
return issues;
},
fix: (content, issues) => {
let fixedContent = content;
issues.forEach(issue => {
if (issue.rule === 'no-unused-vars') {
const varName = issue.message.match(/'([^']+)'/)?.[1];
if (varName) {
const regex = new RegExp(`(?:let|const|var)\\s+${varName}\\s*=\\s*[^;\\n]+;?\\n?`, 'g');
fixedContent = fixedContent.replace(regex, '');
}
}
});
return fixedContent;
}
},
{
name: 'no-console',
description: 'Disallow console statements in production LWC code',
category: 'code-quality',
severity: 'warn',
fixable: true,
check: (content, filePath, config) => {
const issues = [];
const lines = content.split('\n');
lines.forEach((line, index) => {
if (line.includes('console.') && !line.trim().startsWith('//')) {
issues.push({
rule: 'no-console',
message: 'Avoid console statements in production code',
severity: 'warn',
line: index + 1,
fixable: true,
category: 'code-quality'
});
}
});
return issues;
},
fix: (content, issues) => {
return content.replace(/console\.[a-z]+\([^)]*\);?\s*\n?/g, '');
}
},
{
name: 'prefer-const',
description: 'Prefer const over let for variables that are never reassigned',
category: 'code-quality',
severity: 'warn',
fixable: true,
check: (content, filePath, config) => {
const issues = [];
const lines = content.split('\n');
// Find let declarations
const letRegex = /let\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=/g;
let match;
while ((match = letRegex.exec(content)) !== null) {
const varName = match[1];
const reassignmentRegex = new RegExp(`${varName}\\s*=(?!=)`, 'g');
const reassignments = content.match(reassignmentRegex);
// If variable is assigned only once (in declaration), suggest const
if (reassignments && reassignments.length === 1) {
const lineNumber = content.substring(0, match.index).split('\n').length;
issues.push({
rule: 'prefer-const',
message: `Variable '${varName}' is never reassigned. Use 'const' instead of 'let'`,
severity: 'warn',
line: lineNumber,
fixable: true,
category: 'code-quality'
});
}
}
return issues;
},
fix: (content, issues) => {
let fixedContent = content;
issues.forEach(issue => {
if (issue.rule === 'prefer-const') {
const varName = issue.message.match(/'([^']+)'/)?.[1];
if (varName) {
const regex = new RegExp(`let(\\s+${varName}\\s*=)`, 'g');
fixedContent = fixedContent.replace(regex, `const$1`);
}
}
});
return fixedContent;
}
},
{
name: 'no-var',
description: 'Disallow var declarations in favor of let and const',
category: 'code-quality',
severity: 'error',
fixable: true,
check: (content, filePath, config) => {
const issues = [];
const lines = content.split('\n');
lines.forEach((line, index) => {
if (/\bvar\s+/.test(line) && !line.trim().startsWith('//')) {
issues.push({
rule: 'no-var',
message: "Use 'let' or 'const' instead of 'var'",
severity: 'error',
line: index + 1,
fixable: true,
category: 'code-quality'
});
}
});
return issues;
},
fix: (content, issues) => {
return content.replace(/\bvar\b/g, 'let');
}
},
{
name: 'camelcase-naming',
description: 'Enforce camelCase naming convention for LWC components',
category: 'code-quality',
severity: 'warn',
fixable: false,
check: (content, filePath, config) => {
const issues = [];
// Check method names
const methodRegex = /(\w+)\s*\([^)]*\)\s*{/g;
let match;
while ((match = methodRegex.exec(content)) !== null) {
const methodName = match[1];
if (!/^[a-z][a-zA-Z0-9]*$/.test(methodName) &&
!['constructor', 'connectedCallback', 'disconnectedCallback', 'renderedCallback'].includes(methodName)) {
const lineNumber = content.substring(0, match.index).split('\n').length;
issues.push({
rule: 'camelcase-naming',
message: `Method '${methodName}' should be in camelCase`,
severity: 'warn',
line: lineNumber,
fixable: false,
category: 'code-quality'
});
}
}
return issues;
}
},
{
name: 'max-nesting-depth',
description: 'Limit nesting depth to improve readability',
category: 'code-quality',
severity: 'warn',
fixable: false,
check: (content, filePath, config) => {
const issues = [];
const lines = content.split('\n');
const maxDepth = 4;
lines.forEach((line, index) => {
const depth = (line.match(/^\s*/)?.[0].length || 0) / 4; // Assuming 4-space indentation
if (depth > maxDepth) {
issues.push({
rule: 'max-nesting-depth',
message: `Nesting depth of ${depth} exceeds maximum of ${maxDepth}`,
severity: 'warn',
line: index + 1,
fixable: false,
category: 'code-quality'
});
}
});
return issues;
}
},
{
name: 'no-magic-numbers',
description: 'Disallow magic numbers in favor of named constants',
category: 'code-quality',
severity: 'info',
fixable: false,
check: (content, filePath, config) => {
const issues = [];
const lines = content.split('\n');
lines.forEach((line, index) => {
// Look for standalone numbers (not 0, 1, or -1)
const magicNumberRegex = /\b(?!0\b|1\b|-1\b)\d{2,}\b/g;
let match;
while ((match = magicNumberRegex.exec(line)) !== null) {
if (!line.trim().startsWith('//')) {
issues.push({
rule: 'no-magic-numbers',
message: `Magic number '${match[0]}' should be replaced with a named constant`,
severity: 'info',
line: index + 1,
fixable: false,
category: 'code-quality'
});
}
}
});
return issues;
}
}
];
}
//# sourceMappingURL=code-quality.js.map