style: add a "no-shadow" linter rule (#4385)

It has become a common source of mistakes.
For example, during PR #4363 I've referred to the wrong `options`
several times due to the variable shadowing.
This commit is contained in:
Haoqun Jiang
2019-08-02 18:24:52 +08:00
committed by GitHub
parent 2e3fa929fe
commit c76d2e691d
23 changed files with 152 additions and 150 deletions

View File

@@ -15,6 +15,7 @@ module.exports = {
"indent": ["error", 2, {
"MemberExpression": "off"
}],
"no-shadow": ["error"],
"node/no-extraneous-require": ["error", {
"allowModules": [
"@vue/cli-service",

View File

@@ -53,9 +53,9 @@ module.exports.defaultModes = {
'test:e2e': 'production'
}
function removeArg (rawArgs, arg, offset = 1) {
const matchRE = new RegExp(`^--${arg}`)
const equalRE = new RegExp(`^--${arg}=`)
function removeArg (rawArgs, argToRemove, offset = 1) {
const matchRE = new RegExp(`^--${argToRemove}`)
const equalRE = new RegExp(`^--${argToRemove}=`)
const i = rawArgs.findIndex(arg => matchRE.test(arg))
if (i > -1) {
rawArgs.splice(i, offset + (equalRE.test(rawArgs[i]) ? 0 : 1))

View File

@@ -64,9 +64,9 @@ module.exports.defaultModes = {
'test:e2e': 'production'
}
function removeArg (rawArgs, arg, offset = 1) {
const matchRE = new RegExp(`^--${arg}`)
const equalRE = new RegExp(`^--${arg}=`)
function removeArg (rawArgs, argToRemove, offset = 1) {
const matchRE = new RegExp(`^--${argToRemove}`)
const equalRE = new RegExp(`^--${argToRemove}=`)
const i = rawArgs.findIndex(arg => matchRE.test(arg))
if (i > -1) {
rawArgs.splice(i, offset + (equalRE.test(rawArgs[i]) ? 0 : 1))

View File

@@ -26,33 +26,16 @@ module.exports = (api, { config, lintOn = [] }, _, invoking) => {
pkg.devDependencies['babel-eslint'] = '^10.0.1'
}
const injectEditorConfig = (config) => {
const filePath = api.resolve('.editorconfig')
if (fs.existsSync(filePath)) {
// Append to existing .editorconfig
api.render(files => {
const configPath = path.resolve(__dirname, `./template/${config}/_editorconfig`)
const editorconfig = fs.readFileSync(configPath, 'utf-8')
files['.editorconfig'] += `\n${editorconfig}`
})
} else {
api.render(`./template/${config}`)
}
}
if (config === 'airbnb') {
eslintConfig.extends.push('@vue/airbnb')
Object.assign(pkg.devDependencies, {
'@vue/eslint-config-airbnb': '^4.0.0'
})
injectEditorConfig('airbnb')
} else if (config === 'standard') {
eslintConfig.extends.push('@vue/standard')
Object.assign(pkg.devDependencies, {
'@vue/eslint-config-standard': '^4.0.0'
})
injectEditorConfig('standard')
} else if (config === 'prettier') {
eslintConfig.extends.push('@vue/prettier')
Object.assign(pkg.devDependencies, {
@@ -67,6 +50,19 @@ module.exports = (api, { config, lintOn = [] }, _, invoking) => {
eslintConfig.extends.push('eslint:recommended')
}
const editorConfigTemplatePath = path.resolve(__dirname, `./template/${config}/_editorconfig`)
if (fs.existsSync(editorConfigTemplatePath)) {
if (fs.existsSync(api.resolve('.editorconfig'))) {
// Append to existing .editorconfig
api.render(files => {
const editorconfig = fs.readFileSync(editorConfigTemplatePath, 'utf-8')
files['.editorconfig'] += `\n${editorconfig}`
})
} else {
api.render(`./template/${config}`)
}
}
if (!lintOn.includes('save')) {
pkg.vue = {
lintOnSave: false // eslint-loader configured in runtime plugin

View File

@@ -102,18 +102,18 @@ module.exports = api => {
]
}
},
onWrite: async ({ api, prompts, cwd }) => {
onWrite: async ({ onWriteApi, prompts, cwd }) => {
const result = {}
for (const prompt of prompts.filter(p => !p.raw.skipSave)) {
result[`pwa.${prompt.id}`] = await api.getAnswer(prompt.id)
result[`pwa.${prompt.id}`] = await onWriteApi.getAnswer(prompt.id)
}
api.setData('vue', result)
onWriteApi.setData('vue', result)
// Update app manifest
const name = result['name']
if (name) {
api.setData('manifest', {
onWriteApi.setData('manifest', {
name,
short_name: name
})
@@ -121,14 +121,14 @@ module.exports = api => {
const themeColor = result['themeColor']
if (themeColor) {
api.setData('manifest', {
onWriteApi.setData('manifest', {
theme_color: themeColor
})
}
const backgroundColor = await api.getAnswer('backgroundColor')
const backgroundColor = await onWriteApi.getAnswer('backgroundColor')
if (backgroundColor) {
api.setData('manifest', {
onWriteApi.setData('manifest', {
background_color: backgroundColor
})
}

View File

@@ -14,6 +14,7 @@ test('using correct loader', () => {
service.init()
const config = service.resolveWebpackConfig()
// eslint-disable-next-line no-shadow
const rule = config.module.rules.find(rule => rule.test.test('foo.ts'))
expect(rule.use[0].loader).toMatch('cache-loader')
expect(rule.use[1].loader).toMatch('babel-loader')

View File

@@ -1,13 +1,13 @@
const path = require('path')
module.exports = (api, options) => {
module.exports = (api, projectOptions) => {
const fs = require('fs')
const useThreads = process.env.NODE_ENV === 'production' && !!options.parallel
const useThreads = process.env.NODE_ENV === 'production' && !!projectOptions.parallel
api.chainWebpack(config => {
config.resolveLoader.modules.prepend(path.join(__dirname, 'node_modules'))
if (!options.pages) {
if (!projectOptions.pages) {
config.entry('app')
.clear()
.add('./src/main.ts')
@@ -40,8 +40,8 @@ module.exports = (api, options) => {
addLoader({
loader: 'thread-loader',
options:
typeof options.parallel === 'number'
? { workers: options.parallel }
typeof projectOptions.parallel === 'number'
? { workers: projectOptions.parallel }
: {}
})
}
@@ -75,7 +75,7 @@ module.exports = (api, options) => {
.plugin('fork-ts-checker')
.use(require('fork-ts-checker-webpack-plugin'), [{
vue: true,
tslint: options.lintOnSave !== false && fs.existsSync(api.resolve('tslint.json')),
tslint: projectOptions.lintOnSave !== false && fs.existsSync(api.resolve('tslint.json')),
formatter: 'codeframe',
// https://github.com/TypeStrong/ts-loader#happypackmode-boolean-defaultfalse
checkSyntacticErrors: useThreads

View File

@@ -1,88 +1,88 @@
const fs = require('fs')
const path = require('path')
const globby = require('globby')
const tslint = require('tslint')
const ts = require('typescript')
/* eslint-disable-next-line node/no-extraneous-require */
const vueCompiler = require('vue-template-compiler')
const isVueFile = file => /\.vue(\.ts)?$/.test(file)
// hack to make tslint --fix work for *.vue files:
// we save the non-script parts to a cache right before
// linting the file, and patch fs.writeFileSync to combine the fixed script
// back with the non-script parts.
// this works because (luckily) tslint lints synchronously.
const vueFileCache = new Map()
const writeFileSync = fs.writeFileSync
const patchWriteFile = () => {
fs.writeFileSync = (file, content, options) => {
if (isVueFile(file)) {
const parts = vueFileCache.get(path.normalize(file))
if (parts) {
parts.content = content
const { before, after } = parts
content = `${before}\n${content.trim()}\n${after}`
}
}
return writeFileSync(file, content, options)
}
}
const restoreWriteFile = () => {
fs.writeFileSync = writeFileSync
}
const parseTSFromVueFile = file => {
// If the file has already been cached, don't read the file again. Use the cache instead.
if (vueFileCache.has(file)) {
return vueFileCache.get(file)
}
const content = fs.readFileSync(file, 'utf-8')
const { script } = vueCompiler.parseComponent(content, { pad: 'line' })
if (script && /^tsx?$/.test(script.lang)) {
vueFileCache.set(file, {
before: content.slice(0, script.start),
after: content.slice(script.end),
content: script.content
})
return script
}
}
// patch getSourceFile for *.vue files
// so that it returns the <script> block only
const patchProgram = program => {
const getSourceFile = program.getSourceFile
program.getSourceFile = function (file, languageVersion, onError) {
if (isVueFile(file)) {
const { content, lang = 'js' } = parseTSFromVueFile(file) || { content: '', lang: 'js' }
const contentLang = ts.ScriptKind[lang.toUpperCase()]
return ts.createSourceFile(file, content, languageVersion, true, contentLang)
} else {
return getSourceFile.call(this, file, languageVersion, onError)
}
}
}
module.exports = function lint (args = {}, api, silent) {
const cwd = api.resolve('.')
const fs = require('fs')
const path = require('path')
const globby = require('globby')
const tslint = require('tslint')
const ts = require('typescript')
/* eslint-disable-next-line node/no-extraneous-require */
const vueCompiler = require('vue-template-compiler')
const isVueFile = file => /\.vue(\.ts)?$/.test(file)
const options = {
const program = tslint.Linter.createProgram(api.resolve('tsconfig.json'))
patchProgram(program)
const linter = new tslint.Linter({
fix: args['fix'] !== false,
formatter: args.format || 'codeFrame',
formattersDirectory: args['formatters-dir'],
rulesDirectory: args['rules-dir']
}
// hack to make tslint --fix work for *.vue files:
// we save the non-script parts to a cache right before
// linting the file, and patch fs.writeFileSync to combine the fixed script
// back with the non-script parts.
// this works because (luckily) tslint lints synchronously.
const vueFileCache = new Map()
const writeFileSync = fs.writeFileSync
const patchWriteFile = () => {
fs.writeFileSync = (file, content, options) => {
if (isVueFile(file)) {
const parts = vueFileCache.get(path.normalize(file))
if (parts) {
parts.content = content
const { before, after } = parts
content = `${before}\n${content.trim()}\n${after}`
}
}
return writeFileSync(file, content, options)
}
}
const restoreWriteFile = () => {
fs.writeFileSync = writeFileSync
}
const parseTSFromVueFile = file => {
// If the file has already been cached, don't read the file again. Use the cache instead.
if (vueFileCache.has(file)) {
return vueFileCache.get(file)
}
const content = fs.readFileSync(file, 'utf-8')
const { script } = vueCompiler.parseComponent(content, { pad: 'line' })
if (script && /^tsx?$/.test(script.lang)) {
vueFileCache.set(file, {
before: content.slice(0, script.start),
after: content.slice(script.end),
content: script.content
})
return script
}
}
const program = tslint.Linter.createProgram(api.resolve('tsconfig.json'))
// patch getSourceFile for *.vue files
// so that it returns the <script> block only
const patchProgram = program => {
const getSourceFile = program.getSourceFile
program.getSourceFile = function (file, languageVersion, onError) {
if (isVueFile(file)) {
const { content, lang = 'js' } = parseTSFromVueFile(file) || { content: '', lang: 'js' }
const contentLang = ts.ScriptKind[lang.toUpperCase()]
return ts.createSourceFile(file, content, languageVersion, true, contentLang)
} else {
return getSourceFile.call(this, file, languageVersion, onError)
}
}
}
patchProgram(program)
const linter = new tslint.Linter(options, program)
}, program)
// patch linter.updateProgram to ensure every program has correct getSourceFile
const updateProgram = linter.updateProgram
// eslint-disable-next-line no-shadow
linter.updateProgram = function (...args) {
updateProgram.call(this, ...args)
patchProgram(this.program)
@@ -102,7 +102,7 @@ module.exports = function lint (args = {}, api, silent) {
ruleSeverity: 'off'
}))
const lint = file => {
const lintFile = file => {
const filePath = api.resolve(file)
const isVue = isVueFile(file)
patchWriteFile()
@@ -116,7 +116,7 @@ module.exports = function lint (args = {}, api, silent) {
restoreWriteFile()
}
const files = args._ && args._.length
const patterns = args._ && args._.length
? args._
: ['src/**/*.ts', 'src/**/*.vue', 'src/**/*.tsx', 'tests/**/*.ts', 'tests/**/*.tsx']
@@ -125,11 +125,11 @@ module.exports = function lint (args = {}, api, silent) {
// use the raw tslint.json data because config contains absolute paths
const rawTslintConfig = tslint.Configuration.readConfigurationFile(tslintConfigPath)
const excludedGlobs = rawTslintConfig.linterOptions.exclude
excludedGlobs.forEach((g) => files.push('!' + g))
excludedGlobs.forEach((g) => patterns.push('!' + g))
}
return globby(files, { cwd }).then(files => {
files.forEach(lint)
return globby(patterns, { cwd }).then(files => {
files.forEach(lintFile)
if (silent) return
const result = linter.getResult()
if (result.output.trim()) {

View File

@@ -63,13 +63,10 @@ function resolveEntry (entry, cmd) {
}
warnAboutNpmScript(cmd)
return {
context,
entry
}
return entry
}
function createService (context, entry, asLib) {
function createService (entry, asLib) {
return new Service(context, {
projectOptions: {
compiler: true,
@@ -84,15 +81,15 @@ function createService (context, entry, asLib) {
}
exports.serve = (_entry, args) => {
const { context, entry } = resolveEntry(_entry, 'serve')
createService(context, entry).run('serve', args)
const entry = resolveEntry(_entry, 'serve')
createService(entry).run('serve', args)
}
exports.build = (_entry, args) => {
const { context, entry } = resolveEntry(_entry, 'build')
const entry = resolveEntry(_entry, 'build')
const asLib = args.target && args.target !== 'app'
if (asLib) {
args.entry = entry
}
createService(context, entry, asLib).run('build', args)
createService(entry, asLib).run('build', args)
}

View File

@@ -103,7 +103,7 @@ module.exports = function createConfigPlugin (context, entry, asLib) {
.add(/node_modules/)
.end()
.use('eslint-loader')
.tap(options => Object.assign({}, options, {
.tap(loaderOptions => Object.assign({}, loaderOptions, {
useEslintrc: hasESLintConfig,
baseConfig: {
extends: [

View File

@@ -94,11 +94,11 @@ module.exports = class Service {
const basePath = path.resolve(this.context, `.env${mode ? `.${mode}` : ``}`)
const localPath = `${basePath}.local`
const load = path => {
const load = envPath => {
try {
const env = dotenv.config({ path, debug: process.env.DEBUG })
const env = dotenv.config({ path: envPath, debug: process.env.DEBUG })
dotenvExpand(env)
logger(path, env)
logger(envPath, env)
} catch (err) {
// only ignore error if file is not found
if (err.toString().indexOf('ENOENT') < 0) {

View File

@@ -4,7 +4,7 @@ module.exports = function formatStats (stats, dir, api) {
const zlib = require('zlib')
const chalk = require('chalk')
const ui = require('cliui')({ width: 80 })
const url = require('url');
const url = require('url')
const json = stats.toJson({
hash: false,

View File

@@ -4,9 +4,9 @@ module.exports = (api, args, options) => {
// respect inline build destination in copy plugin
if (args.dest && config.plugins.has('copy')) {
config.plugin('copy').tap(args => {
args[0][0].to = targetDir
return args
config.plugin('copy').tap(pluginArgs => {
pluginArgs[0][0].to = targetDir
return pluginArgs
})
}

View File

@@ -4,11 +4,11 @@ const getPadLength = require('../util/getPadLength')
module.exports = (api, options) => {
api.registerCommand('help', args => {
const command = args._[0]
if (!command) {
const commandName = args._[0]
if (!commandName) {
logMainHelp()
} else {
logHelpForCommand(command, api.service.commands[command])
logHelpForCommand(commandName, api.service.commands[commandName])
}
})
@@ -45,11 +45,11 @@ module.exports = (api, options) => {
if (opts.options) {
console.log(`\n Options:\n`)
const padLength = getPadLength(opts.options)
for (const name in opts.options) {
for (const [flags, description] of Object.entries(opts.options)) {
console.log(` ${
chalk.blue(padEnd(name, padLength))
chalk.blue(padEnd(flags, padLength))
}${
opts.options[name]
description
}`)
}
}

View File

@@ -178,6 +178,7 @@ module.exports = (api, options) => {
}, projectDevServerOptions, {
https: useHttps,
proxy: proxySettings,
// eslint-disable-next-line no-shadow
before (app, server) {
// launch editor support.
// this works with vue-devtools & @vue/cli-overlay

View File

@@ -24,14 +24,14 @@ module.exports = (api, options) => {
const outputDir = api.resolve(options.outputDir)
const getAssetPath = require('../util/getAssetPath')
const filename = getAssetPath(
const outputFilename = getAssetPath(
options,
`js/[name]${isLegacyBundle ? `-legacy` : ``}${isProd && options.filenameHashing ? '.[contenthash:8]' : ''}.js`
)
webpackConfig
.output
.filename(filename)
.chunkFilename(filename)
.filename(outputFilename)
.chunkFilename(outputFilename)
// code splitting
if (process.env.NODE_ENV !== 'test') {

View File

@@ -202,7 +202,7 @@ function isArgumentContainsChunkIds (arg) {
function isArgumentContainsModulesList (arg) {
if (arg.type === 'ObjectExpression') {
return arg.properties
.map(arg => arg.value)
.map(prop => prop.value)
.every(isModuleWrapper)
}

View File

@@ -36,14 +36,14 @@ exports.unset = function (target, path) {
if (!obj[key]) {
return
}
objs.splice(0, 0, { obj, key, value: obj[key] })
objs.unshift({ parent: obj, key, value: obj[key] })
obj = obj[key]
}
delete obj[fields[l - 1]]
// Clear empty objects
for (const { obj, key, value } of objs) {
for (const { parent, key, value } of objs) {
if (!Object.keys(value).length) {
delete obj[key]
delete parent[key]
}
}
}

View File

@@ -90,6 +90,7 @@ module.exports = async function serveWithPuppeteer (serve, test, noPuppeteer) {
})
}
/* eslint-disable no-shadow */
function createHelpers (page) {
return {
getText: selector => page.evaluate(selector => {
@@ -106,3 +107,4 @@ function createHelpers (page) {
}, selector, cls)
}
}
/* eslint-enable no-shadow */

View File

@@ -238,12 +238,12 @@ if (!process.argv.slice(2).length) {
program.outputHelp()
}
function suggestCommands (cmd) {
function suggestCommands (unknownCommand) {
const availableCommands = program.commands.map(cmd => {
return cmd._name
})
const suggestion = didYouMean(cmd, availableCommands)
const suggestion = didYouMean(unknownCommand, availableCommands)
if (suggestion) {
console.log(` ` + chalk.red(`Did you mean ${chalk.yellow(suggestion)}?`))
}

View File

@@ -29,12 +29,14 @@ class GeneratorAPI {
this.options = options
this.rootOptions = rootOptions
/* eslint-disable no-shadow */
this.pluginsData = generator.plugins
.filter(({ id }) => id !== `@vue/cli-service`)
.map(({ id }) => ({
name: toShortPluginId(id),
link: getPluginLink(id)
}))
/* eslint-enable no-shadow */
this._entryFile = undefined
}

View File

@@ -3,7 +3,7 @@ const path = require('path')
const homedir = require('os').homedir()
const { get, set, unset, error, launch } = require('@vue/cli-shared-utils')
async function config (value, options) {
async function configure (value, options) {
const file = path.resolve(homedir, '.vuerc')
const config = await fs.readJson(file)
@@ -19,6 +19,7 @@ async function config (value, options) {
}
if (options.get) {
// eslint-disable-next-line no-shadow
const value = get(config, options.get)
if (options.json) {
console.log(JSON.stringify({
@@ -76,7 +77,7 @@ async function config (value, options) {
}
module.exports = (...args) => {
return config(...args).catch(err => {
return configure(...args).catch(err => {
error(err)
if (!process.env.VUE_CLI_TEST) {
process.exit(1)

View File

@@ -1,5 +1,6 @@
module.exports = function stringifyJS (value) {
const stringify = require('javascript-stringify')
// eslint-disable-next-line no-shadow
return stringify(value, (val, indent, stringify) => {
if (val && val.__expression) {
return val.__expression