» Make grep CLI App in Node.js » 2. Development » 2.6 Add Lint

Add Lint

Lint refers to a tool or program that analyzes source code to find and report issues related to programming style, potential errors, and adherence to coding standards. The term "lint" originates from the name of a Unix utility called "lint" that was used to identify bugs and errors in C programming code.

Using linting tools in a Node.js project is a good practice for maintaining code quality and consistency. Linting tools analyze your code for potential errors, style issues, and adherence to coding standards.

There are several linting tools available for JavaScript. Two popular ones are ESLint and JSHint. We'll use ESLint in this project.

Install it using:

npm install eslint --save-dev

Create a configuration file for your 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/"],
};

Customize the linting rules in your configuration file based on your project's requirements. Rules define coding standards, error handling, and style preferences.

And then run it to lint the code you care about:

npx eslint lib/*ts bin/*ts

You can add a linting script in your package.json file:

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

You will get a list of lint issues:

/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
...

Try to fix all of them and run lint again.

npm run lint

If nothing pops out, your code is in "good style" now.

Changes in 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 []
 }

Changes in 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)
       }
     }
   }
PrevNext