@blaze-money/eslint-plugin-spark
Version:
ESLint plugin for Spark project with custom rules
253 lines (231 loc) • 8.87 kB
JavaScript
module.exports = {
meta: {
type: "problem",
docs: {
description:
"Enforce using ConfigService.get with EnvironmentVariables enum instead of direct process.env access",
category: "Best Practices",
recommended: true,
},
fixable: "code",
hasSuggestions: true,
schema: [
{
type: "object",
properties: {
excludedFiles: {
type: "array",
items: {
type: "string",
},
},
},
additionalProperties: false,
},
],
},
create(context) {
// Get excluded files from rule options
const options = context.options[0] || {}
const excludedFiles = options.excludedFiles || []
// Get current filename
const filename = context.getFilename()
// Check if current file is excluded
const isExcluded = excludedFiles.some(excludedFile =>
filename.endsWith(excludedFile)
)
// Skip rule if file is excluded
if (isExcluded) {
return {}
}
// Track if we've seen an import for EnvironmentVariables
let hasEnvironmentVariablesImport = false
// Keep track of whether we need to add import
let needsImport = false
// Keep track of violations for potential batch fixing
let violations = []
// Store potential import locations
let configImportNode = null
let firstImportNode = null
// Check if this.config or this.configService is used in the file
let hasConfigServiceReference = false
return {
// Check for existing imports of EnvironmentVariables
ImportDeclaration(node) {
// Store the first import we encounter for later use
if (!firstImportNode) {
firstImportNode = node
}
if (
node.source.value === "@config.module" ||
node.source.value === "@app/modules/App/config.module" ||
node.source.value.includes("config.module")
) {
// Store this as a potential location to add EnvironmentVariables import
configImportNode = node
// Check if EnvironmentVariables is one of the imported specifiers
for (const specifier of node.specifiers) {
if (
specifier.type === "ImportSpecifier" &&
specifier.imported &&
specifier.imported.name === "EnvironmentVariables"
) {
hasEnvironmentVariablesImport = true
break
}
}
}
// Check for ConfigService import
if (node.source.value === "@nestjs/config") {
for (const specifier of node.specifiers) {
if (
specifier.type === "ImportSpecifier" &&
specifier.imported &&
specifier.imported.name === "ConfigService"
) {
// NestJS Config is imported
break
}
}
}
},
// Check for this.config or this.configService references
MemberExpression(node) {
if (
node.object.type === "ThisExpression" &&
node.property.type === "Identifier" &&
(node.property.name === "config" ||
node.property.name === "configService")
) {
hasConfigServiceReference = true
}
},
// Check for process.env access
MemberExpression(node) {
if (
node.object.type === "MemberExpression" &&
node.object.object.type === "Identifier" &&
node.object.object.name === "process" &&
node.object.property.type === "Identifier" &&
node.object.property.name === "env" &&
node.property.type === "Identifier"
) {
const envVarName = node.property.name
// Add to violations array
violations.push({
node,
envVarName,
})
// Check if we need to add import
if (!hasEnvironmentVariablesImport) {
needsImport = true
}
context.report({
node,
message:
"Use ConfigService.get with EnvironmentVariables enum instead of direct process.env access",
*fix(fixer) {
// We need to be careful here - don't automatically fix if we don't have ConfigService
if (!hasConfigServiceReference) {
// If we can't confirm there's a this.config/this.configService, don't auto-fix
return null
}
// Replace process.env.X with this.config.get(EnvironmentVariables.X)
yield fixer.replaceText(
node.parent.type === "MemberExpression" ? node.parent : node,
`this.config.get(EnvironmentVariables.${envVarName})`
)
// If we need to add an import and this is the first violation being fixed
if (needsImport && node === violations[0].node) {
// If we found an existing config module import, add EnvironmentVariables to it
if (configImportNode) {
// If this is a named import with {}, add to it
const hasNamedImports = configImportNode.specifiers.some(
s => s.type === "ImportSpecifier"
)
if (hasNamedImports) {
// Find the closing brace of the import specifiers
const sourceCode = context.getSourceCode()
const text = sourceCode.getText(configImportNode)
const openBraceIndex = text.indexOf("{")
const closeBraceIndex = text.indexOf("}")
if (openBraceIndex >= 0 && closeBraceIndex >= 0) {
// If there are existing specifiers, add a comma
const hasSpecifiers =
configImportNode.specifiers.filter(
s => s.type === "ImportSpecifier"
).length > 0
const insertText = hasSpecifiers
? ", EnvironmentVariables"
: "EnvironmentVariables"
yield fixer.insertTextBeforeRange(
[
configImportNode.range[0] + closeBraceIndex,
configImportNode.range[0] + closeBraceIndex,
],
insertText
)
}
} else {
// If it's a default import only, add a named import part
yield fixer.insertTextAfter(
configImportNode,
", { EnvironmentVariables }"
)
}
} else if (firstImportNode) {
// Add a new import statement before the first import
yield fixer.insertTextBefore(
firstImportNode,
`import { EnvironmentVariables } from "@app/modules/App/config.module"\n`
)
} else {
// No imports found, add at the beginning of the file
yield fixer.insertTextBefore(
context.getSourceCode().ast,
`import { EnvironmentVariables } from "@app/modules/App/config.module"\n\n`
)
}
}
},
suggest: [
{
desc: `Replace with this.config.get(EnvironmentVariables.${envVarName})`,
fix: fixer => {
return fixer.replaceText(
node.parent.type === "MemberExpression"
? node.parent
: node,
`this.config.get(EnvironmentVariables.${envVarName})`
)
},
},
{
desc: "Add EnvironmentVariables import and replace",
fix: fixer => {
const fixes = [
fixer.replaceText(
node.parent.type === "MemberExpression"
? node.parent
: node,
`this.config.get(EnvironmentVariables.${envVarName})`
),
]
if (!hasEnvironmentVariablesImport && firstImportNode) {
fixes.push(
fixer.insertTextBefore(
firstImportNode,
`import { EnvironmentVariables } from "@app/modules/App/config.module"\n`
)
)
}
return fixes
},
},
],
})
}
},
}
},
}