» Node.js创建命令行程序grep » 2. 开发 » 2.6 添加 Lint

添加 Lint

Lint 是分析源代码并查找、报告出有关编程风格、潜在错误和编码标准等问题的工具。术语 "lint" 源自一个名为 "lint" 的 Unix 实用工具,它用于识别 C 代码中的错误和缺陷。

在 Node.js 项目中使用 linting 工具是一种最佳实践,它有助于保证代码质量并保持代码一致性。JavaScript 中比较流行的两个 linting 工具是 ESLintJSHint。在本项目中,我们将使用 ESLint

如下命令安装:

npm install eslint --save-dev

为 linter 创建配置文件:

npx eslint --init

.eslintrc.cjs:

module.exports = {
  env: {
    browser: true,
    es2021: true,
  },
  extends: "standard-with-typescript",
  overrides: [
    {
      env: {
        node: true,
      },
      files: [".eslintrc.{js,cjs}"],
      parserOptions: {
        sourceType: "script",
      },
    },
  ],
  parserOptions: {
    ecmaVersion: "latest",
    sourceType: "module",
  },
  rules: {},
  ignorePatterns: ["*test.ts", ".eslintrc.*js", "dist/", "node_modules/"],
};

根据项目需求再配置文件中自定义 linting 规则。规则包括代码标准、错误处理和风格偏向等。

然后运行如下命令检查相关业务代码:

npx eslint lib/*ts bin/*ts

可以在 package.json 中添加 linting 脚本:

"scripts": {
    ...
    "lint": "npx eslint lib/*ts bin/*ts",
    ...

得到问题列表:

/Users/netdong/workspace/2023/projects/lr_grepjs/bin/cmd.ts
   3:19  error  Strings must use singlequote                                                          @typescript-eslint/quotes
   3:26  error  Extra semicolon                                                                       @typescript-eslint/semi
   5:1   error  Import "MatchResult" is only used as types                                            @typescript-eslint/consistent-type-imports
   5:61  error  Strings must use singlequote                                                          @typescript-eslint/quotes
   5:77  error  Extra semicolon                                                                       @typescript-eslint/semi
  10:10  error  Strings must use singlequote                                                          @typescript-eslint/quotes
  11:11  error  Strings must use singlequote                                                          @typescript-eslint/quotes
  12:12  error  Strings must use singlequote                                                          @typescript-eslint/quotes
  13:15  error  Strings must use singlequote                                                          @typescript-eslint/quotes
...

尝试修复如上问题并再次执行 lint。

npm run lint

如果没有新的问题蹦出来,那么表明你的代码处于“良好”状态。

lib/grep.ts 中的更改:

@@ -1,70 +1,67 @@
+import fs from 'fs'
+import path from 'path'
 
-import fs from 'fs';
-import path from 'path';
-
-type Options  = {
-  ignoreCase: boolean;
-  invertMatch: boolean;
+interface Options {
+  ignoreCase: boolean
+  invertMatch: boolean
 }
 
-type MatchItem = [number, string];
+type MatchItem = [number, string]
 
-export type MatchResult = {
-  [key: string]: MatchItem[];
-}
+export type MatchResult = Record<string, MatchItem[]>
 
-export async function grep(pattern: string, filePath: string, options: Options) {
-  const { ignoreCase, invertMatch } = options;
-  const lines = await _readFileLines(filePath);
-  const regexFlags = ignoreCase ? "gi" : "g";
-  const regex = new RegExp(pattern, regexFlags);
+export async function grep (pattern: string, filePath: string, options: Options): Promise<MatchResult> {
+  const { ignoreCase, invertMatch } = options
+  const lines = await _readFileLines(filePath)
+  const regexFlags = ignoreCase ? 'gi' : 'g'
+  const regex = new RegExp(pattern, regexFlags)
   let matchingLines: MatchItem[]
   if (invertMatch) {
-    matchingLines = _filterLines(regex, lines, false);
+    matchingLines = _filterLines(regex, lines, false)
   } else {
-    matchingLines = _filterLines(regex, lines, true);
+    matchingLines = _filterLines(regex, lines, true)
   }
-  return { [filePath]: matchingLines };
+  return { [filePath]: matchingLines }
 }
 
-export async function grepRecursive(pattern: string, dirPath: string, options: Options) {
-  let results = {};
+export async function grepRecursive (pattern: string, dirPath: string, options: Options): Promise<MatchResult> {
+  let results = {}
   try {
-    const files = await fs.promises.readdir(dirPath);
+    const files = await fs.promises.readdir(dirPath)
     for (const file of files) {
-      const filePath = path.join(dirPath, file);
-      const isSubDir = (await fs.promises.stat(filePath)).isDirectory();
+      const filePath = path.join(dirPath, file)
+      const isSubDir = (await fs.promises.stat(filePath)).isDirectory()
       const result = !isSubDir
         ? await grep(pattern, filePath, options)
-        : await grepRecursive(pattern, filePath, options);
-      results = { ...results, ...result };
+        : await grepRecursive(pattern, filePath, options)
+      results = { ...results, ...result }
     }
   } catch (err) {
-    console.error(err);
+    console.error(err)
   }
-  return results;
+  return results
 }
 
-export function grepCount(result: MatchResult) {
+export function grepCount (result: MatchResult): number {
   return Object.values(result).reduce(
     (count, lines) => count + lines.length,
     0
-  );
+  )
 }
 
-function _filterLines(regexPattern: RegExp, lines: string[], flag: boolean): MatchItem[] {
-  const candidates: MatchItem[] = lines.map((line, index) => [index + 1, line.trim()]);
+function _filterLines (regexPattern: RegExp, lines: string[], flag: boolean): MatchItem[] {
+  const candidates: MatchItem[] = lines.map((line, index) => [index + 1, line.trim()])
   return candidates
-    .filter(([_, line]) => regexPattern.test(line) === flag);
+    .filter(([_, line]) => regexPattern.test(line) === flag)
 }
 
-async function _readFileLines(filePath: string) {
+async function _readFileLines (filePath: string): Promise<string[]> {
   try {
     // Read the file asynchronously
-    const data = await fs.promises.readFile(filePath, "utf8");
-    return data.split("\n");
+    const data = await fs.promises.readFile(filePath, 'utf8')
+    return data.split('\n')
   } catch (error: any) {
-    console.error("Error reading the file:", error.message);
+    console.error('Error reading the file:', error.message)
   }
-  return [];
+  return []
 }

bin/cmd.ts 中的更改:

@@ -1,97 +1,98 @@
 #!/usr/bin/env node
 
-import yargs from "yargs";
+import yargs from 'yargs'
 
-import { grep, grepCount, grepRecursive, MatchResult } from "../lib/grep.js";
+import { grep, grepCount, grepRecursive } from '../lib/grep.js'
+import type { MatchResult } from '../lib/grep.js'
 
 // Parse command-line options
 const argv = await yargs(process.argv.slice(2))
   .locale('en')
-  .usage("Usage: $0 [options] <pattern> <file>")
-  .option("c", {
-    alias: "count",
-    describe: "Only a count of selected lines is written to standard output.",
-    type: "boolean",
-    default: false,
+  .usage('Usage: $0 [options] <pattern> <file>')
+  .option('c', {
+    alias: 'count',
+    describe: 'Only a count of selected lines is written to standard output.',
+    type: 'boolean',
+    default: false
   })
-  .option("h", {
-    alias: "help",
-    describe: "Print a brief help message.",
-    type: "boolean",
-    default: false,
+  .option('h', {
+    alias: 'help',
+    describe: 'Print a brief help message.',
+    type: 'boolean',
+    default: false
   })
-  .option("i", {
-    alias: "ignore-case",
+  .option('i', {
+    alias: 'ignore-case',
     describe:
-      "Perform case insensitive matching. By default, it is case sensitive.",
-    type: "boolean",
-    default: false,
+      'Perform case insensitive matching. By default, it is case sensitive.',
+    type: 'boolean',
+    default: false
   })
-  .option("n", {
-    alias: "line-number",
+  .option('n', {
+    alias: 'line-number',
     describe:
-      "Each output line is preceded by its relative line number in the file, starting at line 1. The line number counter is reset for each file processed. This option is ignored if -c is specified.",
-    type: "boolean",
-    default: false,
+      'Each output line is preceded by its relative line number in the file, starting at line 1. The line number counter is reset for each file processed. This option is ignored if -c is specified.',
+    type: 'boolean',
+    default: false
   })
-  .option("r", {
-    alias: "recursive",
-    describe: "Recursively search subdirectories listed.",
-    type: "boolean",
-    default: false,
+  .option('r', {
+    alias: 'recursive',
+    describe: 'Recursively search subdirectories listed.',
+    type: 'boolean',
+    default: false
   })
-  .option("v", {
-    alias: "invert-match",
+  .option('v', {
+    alias: 'invert-match',
     describe:
-      "Selected lines are those not matching any of the specified patterns.",
-    type: "boolean",
-    default: false,
+      'Selected lines are those not matching any of the specified patterns.',
+    type: 'boolean',
+    default: false
   })
-  .demandCommand(2, "Please provide both pattern and file arguments.").argv;
+  .demandCommand(2, 'Please provide both pattern and file arguments.').argv
 
-const pattern = argv._[0] as string;
-const filePath = argv._[1] as string;
+const pattern = argv._[0] as string
+const filePath = argv._[1] as string
 
-if (argv.help) {
+if (argv.help as boolean) {
   // Print help message and exit
-  console.log(argv.help);
-  process.exit(0);
+  console.log(argv.help)
+  process.exit(0)
 }
 
 const options = {
-  ignoreCase: argv["ignore-case"] as boolean,
-  invertMatch: argv["invert-match"] as boolean,
-};
-const result = argv.recursive
+  ignoreCase: argv['ignore-case'] as boolean,
+  invertMatch: argv['invert-match'] as boolean
+}
+const result = argv.recursive as boolean
   ? grepRecursive(pattern, filePath, options)
-  : grep(pattern, filePath, options);
+  : grep(pattern, filePath, options)
 
 result
   .then((result) => {
-    if (argv.count) {
-      console.log(grepCount(result));
+    if (argv.count as boolean) {
+      console.log(grepCount(result))
     } else {
-      printResult(result, argv["line-number"] as boolean);
+      printResult(result, argv['line-number'] as boolean)
     }
   })
   .catch((error) => {
-    console.error("Error:", error.message);
-  });
+    console.error('Error:', error.message)
+  })
 
-function printResult(result: MatchResult, showLineNumber: boolean) {
-  let currentFile = null;
-  const fileCount = Object.keys(result).length;
+function printResult (result: MatchResult, showLineNumber: boolean): void {
+  let currentFile = null
+  const fileCount = Object.keys(result).length
 
   for (const [filePath, lines] of Object.entries(result)) {
     for (const [lineNumber, line] of lines) {
       if (fileCount > 1 && filePath !== currentFile) {
-        currentFile = filePath;
-        console.log(`\n${filePath}:`);
+        currentFile = filePath
+        console.log(`\n${filePath}:`)
       }
       if (showLineNumber) {
-        console.log(`${lineNumber}: ${line}`);
+        console.log(`${lineNumber}: ${line}`)
       } else {
-        console.log(line);
+        console.log(line)
       }
     }
   }