UNPKG

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
"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