UNPKG

pi-lens

Version:

Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo

203 lines (202 loc) • 7.91 kB
import * as path from "node:path"; import { safeSpawnAsync } from "../../safe-spawn.js"; import { createAvailabilityChecker } from "./utils/runner-helpers.js"; import { PRIORITY } from "../priorities.js"; const dart = createAvailabilityChecker("dart", ".exe"); const flutter = createAvailabilityChecker("flutter", ".bat"); // Lint rules that `dart fix` rewrites deterministically. The `--format= // machine` output does NOT surface a per-diagnostic fix flag, so we keep // a curated allowlist. Sources: dart.dev/tools/dart-fix and the `linter` // package's documentation of which rules ship a fix. Conservative — when // in doubt, leave a rule off this list. const DART_FIXABLE_RULES = new Set([ // const / final canonicalization "prefer_const_constructors", "prefer_const_constructors_in_immutables", "prefer_const_declarations", "prefer_const_literals_to_create_immutables", "prefer_final_fields", "prefer_final_in_for_each", "prefer_final_locals", "prefer_final_parameters", "unnecessary_const", "unnecessary_final", "unnecessary_new", // null-aware / null safety modernization "avoid_init_to_null", "avoid_null_checks_in_equality_operators", "avoid_returning_null_for_void", "null_closures", "prefer_if_null_operators", "prefer_null_aware_operators", "unnecessary_null_aware_assignments", "unnecessary_null_in_if_null_operators", // string / quoting / interpolation "prefer_adjacent_string_concatenation", "prefer_interpolation_to_compose_strings", "prefer_single_quotes", "unnecessary_brace_in_string_interps", "unnecessary_string_escapes", "unnecessary_string_interpolations", "use_raw_strings", "use_string_buffers", // collection literals / spread / where "prefer_collection_literals", "prefer_contains", "prefer_for_elements_to_map_fromiterable", "prefer_inlined_adds", "prefer_int_literals", "prefer_iterable_whereType", "prefer_is_empty", "prefer_is_not_empty", "prefer_is_not_operator", "prefer_spread_collections", "use_collection_literals_when_possible", // types / generics / function syntax "omit_local_variable_types", "prefer_expression_function_bodies", "prefer_function_declarations_over_variables", "prefer_generic_function_type_aliases", "prefer_typing_uninitialized_variables", "prefer_void_to_null", "type_init_formals", "use_function_type_syntax_for_parameters", // redundancy / cleanup "avoid_redundant_argument_values", "avoid_unused_constructor_parameters", "empty_catches", "empty_constructor_bodies", "unnecessary_lambdas", "unnecessary_overrides", "unnecessary_parenthesis", "unnecessary_this", // imports / sorting / formatting "combinators_ordering", "directives_ordering", "prefer_relative_imports", "sort_child_properties_last", "slash_for_doc_comments", // flutter-specific deterministic rewrites "sized_box_for_whitespace", "use_decorated_box", "use_full_hex_values_for_flutter_colors", "use_key_in_widget_constructors", "use_named_constants", "use_super_parameters", // misc "prefer_conditional_assignment", "prefer_equal_for_default_values", "prefer_initializing_formals", "avoid_void_async", "unawaited_futures", "use_rethrow_when_possible", ]); // dart analyze --format=machine output: // severity|type|code|file|line|col|length|message function parseDartMachineOutput(raw, filePath) { const diagnostics = []; for (const line of raw.split(/\r?\n/)) { if (!line.trim()) continue; const parts = line.split("|"); if (parts.length < 8) continue; const [severityStr, , code, file, lineStr, colStr, , ...messageParts] = parts; const message = messageParts.join("|").trim(); const lineNum = parseInt(lineStr, 10); const colNum = parseInt(colStr, 10); // Only include diagnostics for the target file if (file && !path.resolve(file).endsWith(path.resolve(filePath).replace(/\\/g, "/"))) { const resolvedFile = path.resolve(file.trim()); const resolvedTarget = path.resolve(filePath); if (resolvedFile !== resolvedTarget) continue; } const severity = severityStr?.trim().toLowerCase() === "error" ? "error" : "warning"; const ruleId = code?.trim() ?? "dart"; const fixable = DART_FIXABLE_RULES.has(ruleId); diagnostics.push({ id: `dart-${ruleId}-${lineNum}-${colNum}`, message: `[${ruleId}] ${message}`, filePath, line: lineNum || 1, column: colNum || 1, severity, semantic: severity === "error" ? "blocking" : "warning", tool: "dart", rule: ruleId, defectClass: "style", fixable, fixSuggestion: fixable ? "Run `dart fix --apply` to apply the deterministic auto-correction for this rule." : undefined, }); } return diagnostics; } function firstOutputLine(result) { return `${result.stderr || ""}\n${result.stdout || ""}` .trim() .split(/\r?\n/, 1)[0] .slice(0, 200); } const dartAnalyzeRunner = { id: "dart-analyze", appliesTo: ["dart"], priority: PRIORITY.GENERAL_ANALYSIS, enabledByDefault: true, skipTestFiles: false, async run(ctx) { const cwd = ctx.cwd || process.cwd(); const absPath = path.resolve(cwd, ctx.filePath); const dartAvailable = await dart.isAvailableAsync(cwd); const flutterAvailable = !dartAvailable && (await flutter.isAvailableAsync(cwd)); if (!dartAvailable && !flutterAvailable) { return { status: "skipped", diagnostics: [], semantic: "none" }; } const cmd = dartAvailable ? dart.getCommand(cwd) : flutter.getCommand(cwd); const args = dartAvailable ? ["analyze", "--format=machine", absPath] : ["analyze", "--machine", absPath]; const result = await safeSpawnAsync(cmd, args, { cwd, timeout: 30000 }); if (result.error && !result.stdout && !result.stderr) { return { status: "skipped", diagnostics: [], semantic: "none" }; } // dart analyze writes diagnostics to stderr in machine format const raw = (result.stderr || "") + (result.stdout || ""); const diagnostics = parseDartMachineOutput(raw, ctx.filePath); if (diagnostics.length === 0) { if (result.status && result.status !== 0) { return { status: "failed", diagnostics: [ { id: "dart-analyze-nonzero-no-diagnostics", message: firstOutputLine(result) || "dart analyze exited non-zero without machine diagnostics", filePath: ctx.filePath, severity: "warning", semantic: "warning", tool: "dart", rule: "dart-analyze", fixable: false, }, ], semantic: "warning", }; } return { status: "succeeded", diagnostics: [], semantic: "none" }; } const hasErrors = diagnostics.some((d) => d.severity === "error"); return { status: hasErrors ? "failed" : "succeeded", diagnostics, semantic: hasErrors ? "blocking" : "warning", }; }, }; export default dartAnalyzeRunner; export { parseDartMachineOutput, DART_FIXABLE_RULES };