All files / src/adapters stylelint.ts

43.3% Statements 55/127
100% Branches 1/1
16.66% Functions 1/6
43.3% Lines 55/127

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 1282x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x                         2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x               2x 2x 2x 3x 3x 2x 2x 2x             2x 2x     2x 2x                                                                                           2x  
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import type { LinterAdapter, AdapterResult, LinterDiagnostic, Severity } from './adapter';
import { getStylelintConfigPath } from '../config';
import { stylelintExtraRules } from '../rules/builtin-rules';
 
const CONFIG_FILE_NAMES = [
  '.stylelintrc',
  '.stylelintrc.json',
  '.stylelintrc.yaml',
  '.stylelintrc.yml',
  '.stylelintrc.js',
  'stylelint.config.js',
  'stylelint.config.mjs',
  'stylelint.config.cjs',
];
 
async function getDefaultConfig(): Promise<Record<string, unknown>> {
  const mod = await import('stylelint-config-recommended');
  const recommendedConfig = (mod.default ?? mod) as Record<string, unknown>;
  const recommendedRules = (recommendedConfig.rules ?? {}) as Record<string, unknown>;
  return {
    ...recommendedConfig,
    rules: {
      ...recommendedRules,
      ...stylelintExtraRules,
    },
  };
}
 
interface LinterOptions {
  code?: string;
  codeFilename?: string;
  cwd?: string;
  config?: Record<string, unknown>;
  configFile?: string;
}
 
interface LinterResult {
  results: Array<{
    warnings: Array<{
      line: number;
      column: number;
      endLine?: number;
      endColumn?: number;
      rule: string;
      severity: string;
      text: string;
      fix?: { range: [number, number]; text: string };
    }>;
  }>;
}
 
function hasExternalConfig(dir: string): boolean {
  try {
    return CONFIG_FILE_NAMES.some(name => fs.existsSync(path.join(dir, name)));
  } catch {
    return false;
  }
}
 
export class StylelintAdapter implements LinterAdapter {
  id = 'stylelint';
  supportedLanguages = ['css'];
 
  private _module: { lint: (opts: LinterOptions) => Promise<LinterResult> } | undefined;
 
  private async getModule(): Promise<{ lint: (opts: LinterOptions) => Promise<LinterResult> }> {
    if (!this._module) {
      const mod = await import('stylelint');
      this._module = (mod.default ?? mod) as { lint: (opts: LinterOptions) => Promise<LinterResult> };
    }
    return this._module;
  }
 
  isAvailable(): boolean {
    return true;
  }
 
  async check(document: vscode.TextDocument, workingDir: string): Promise<AdapterResult> {
    try {
      const stylelint = await this.getModule();

      const lintOptions: LinterOptions = {
        code: document.getText(),
        codeFilename: document.fileName,
        cwd: workingDir,
      };

      const globalPath = getStylelintConfigPath();
      if (globalPath && globalPath.trim() !== '') {
        lintOptions.configFile = globalPath;
      } else if (!hasExternalConfig(workingDir)) {
        lintOptions.config = await getDefaultConfig();
      }

      const result = await stylelint.lint(lintOptions);

      const diagnostics: LinterDiagnostic[] = [];
      for (const res of result.results) {
        for (const w of res.warnings) {
          diagnostics.push({
            severity: w.severity as Severity,
            ruleId: `stylelint:${w.rule}`,
            message: w.text,
            range: new vscode.Range(
              w.line - 1,
              w.column - 1,
              (w.endLine ?? w.line) - 1,
              (w.endColumn ?? w.column) - 1
            ),
            fix: w.fix ? { range: [w.fix.range[0], w.fix.range[1]], text: w.fix.text } : undefined,
          });
        }
      }

      return { diagnostics, status: 'ok' };
    } catch (error) {
      return {
        diagnostics: [],
        status: 'execution-failed',
        errorMessage: error instanceof Error ? error.message : String(error),
      };
    }
  }
}