feat(patch): added language-specific import management with deduplication
- Added language-specific import management for TypeScript, JavaScript, Python, Rust, Go, and C/C++ with automatic deduplication and grouping. - Added `imports` parameter to patch, hashline, and replace edit operations for structured import/include requests with named, default, namespace, and alias support. - Added `edit.manageImports` setting to control post-edit import management behavior across all edit operations. - Implemented language-specific import handlers with parsing, merging, and rendering logic for each supported language. - Integrated import application into EditTool with feature flag support and comprehensive test coverage across all languages.
This commit is contained in:
@@ -0,0 +1,653 @@
|
||||
import type { ImportSpec } from "../types";
|
||||
import {
|
||||
defaultImportStyle,
|
||||
type ImportHandler,
|
||||
type ImportRegion,
|
||||
type ImportStyle,
|
||||
type ParsedImport,
|
||||
} from "./types";
|
||||
|
||||
type ImportGroup = "stdlib" | "thirdparty" | "local";
|
||||
|
||||
interface PythonImportStyle extends ImportStyle {
|
||||
parenthesized: boolean;
|
||||
}
|
||||
|
||||
interface ImportedName {
|
||||
name: string;
|
||||
alias?: string;
|
||||
order: number;
|
||||
}
|
||||
|
||||
interface ParsedPythonImport extends ParsedImport {
|
||||
kind: "module" | "from";
|
||||
group: ImportGroup;
|
||||
order: number;
|
||||
multiline: boolean;
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
names: string[];
|
||||
parsedNames: ImportedName[];
|
||||
}
|
||||
|
||||
interface ParsedPythonRegion extends ImportRegion {
|
||||
imports: ParsedPythonImport[];
|
||||
style: PythonImportStyle;
|
||||
}
|
||||
|
||||
const DEFAULT_GROUP_ORDER: ImportGroup[] = ["stdlib", "thirdparty", "local"];
|
||||
|
||||
const STDLIB_MODULES = new Set<string>([
|
||||
"__future__",
|
||||
"abc",
|
||||
"argparse",
|
||||
"array",
|
||||
"ast",
|
||||
"asyncio",
|
||||
"base64",
|
||||
"collections",
|
||||
"contextlib",
|
||||
"copy",
|
||||
"csv",
|
||||
"dataclasses",
|
||||
"datetime",
|
||||
"decimal",
|
||||
"enum",
|
||||
"functools",
|
||||
"glob",
|
||||
"gzip",
|
||||
"hashlib",
|
||||
"heapq",
|
||||
"hmac",
|
||||
"html",
|
||||
"http",
|
||||
"importlib",
|
||||
"inspect",
|
||||
"io",
|
||||
"itertools",
|
||||
"json",
|
||||
"logging",
|
||||
"math",
|
||||
"mimetypes",
|
||||
"os",
|
||||
"pathlib",
|
||||
"pickle",
|
||||
"platform",
|
||||
"queue",
|
||||
"random",
|
||||
"re",
|
||||
"secrets",
|
||||
"shlex",
|
||||
"shutil",
|
||||
"socket",
|
||||
"sqlite3",
|
||||
"statistics",
|
||||
"string",
|
||||
"subprocess",
|
||||
"sys",
|
||||
"tempfile",
|
||||
"threading",
|
||||
"time",
|
||||
"traceback",
|
||||
"types",
|
||||
"typing",
|
||||
"unittest",
|
||||
"urllib",
|
||||
"uuid",
|
||||
"warnings",
|
||||
"weakref",
|
||||
"xml",
|
||||
"zipfile",
|
||||
"zoneinfo",
|
||||
]);
|
||||
|
||||
function getPythonStyle(style: ImportStyle): PythonImportStyle {
|
||||
return {
|
||||
...style,
|
||||
parenthesized: (style as Partial<PythonImportStyle>).parenthesized ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
function detectEol(content: string): string {
|
||||
return content.includes("\r\n") ? "\r\n" : "\n";
|
||||
}
|
||||
|
||||
function isBlankLine(line: string): boolean {
|
||||
return line.trim().length === 0;
|
||||
}
|
||||
|
||||
function isCommentLine(line: string): boolean {
|
||||
return line.trimStart().startsWith("#");
|
||||
}
|
||||
|
||||
function isEncodingComment(line: string): boolean {
|
||||
return /^[ \t]*#.*coding[:=][ \t]*[-\w.]+/.test(line);
|
||||
}
|
||||
|
||||
function isImportStart(line: string): boolean {
|
||||
const trimmed = line.trimStart();
|
||||
return trimmed.startsWith("import ") || trimmed.startsWith("from ");
|
||||
}
|
||||
|
||||
function stripInlineComment(line: string): string {
|
||||
const hashIndex = line.indexOf("#");
|
||||
return hashIndex === -1 ? line : line.slice(0, hashIndex);
|
||||
}
|
||||
|
||||
function parseModuleDocstring(lines: string[], startIndex: number): number | null {
|
||||
const firstLine = lines[startIndex];
|
||||
if (firstLine === undefined) return null;
|
||||
const trimmed = firstLine.trimStart();
|
||||
const match = /^[rRuUbBfF]*(["']{3})/.exec(trimmed);
|
||||
if (!match) return null;
|
||||
const delimiter = match[1];
|
||||
const remainder = trimmed.slice(match[0].length);
|
||||
if (remainder.includes(delimiter)) return startIndex + 1;
|
||||
for (let index = startIndex + 1; index < lines.length; index += 1) {
|
||||
if (lines[index]?.includes(delimiter)) return index + 1;
|
||||
}
|
||||
return lines.length;
|
||||
}
|
||||
|
||||
function findPrefixEnd(lines: string[]): number {
|
||||
let index = 0;
|
||||
if (lines[0]?.startsWith("#!")) index += 1;
|
||||
if (isEncodingComment(lines[index] ?? "")) index += 1;
|
||||
while (index < lines.length && isBlankLine(lines[index] ?? "")) index += 1;
|
||||
const docstringEnd = parseModuleDocstring(lines, index);
|
||||
if (docstringEnd !== null) {
|
||||
index = docstringEnd;
|
||||
while (index < lines.length && isBlankLine(lines[index] ?? "")) index += 1;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
function classifyGroup(source: string): ImportGroup {
|
||||
if (source.startsWith(".")) return "local";
|
||||
const root = source.split(".")[0] ?? source;
|
||||
return STDLIB_MODULES.has(root) ? "stdlib" : "thirdparty";
|
||||
}
|
||||
|
||||
function formatImportedName(name: ImportedName): string {
|
||||
return name.alias ? `${name.name} as ${name.alias}` : name.name;
|
||||
}
|
||||
|
||||
function parseImportedName(token: string, order: number): ImportedName | null {
|
||||
const cleaned = stripInlineComment(token).replace(/,$/, "").trim();
|
||||
if (cleaned.length === 0) return null;
|
||||
const match = /^(\*|[A-Za-z_][\w]*)(?:\s+as\s+([A-Za-z_][\w]*))?$/.exec(cleaned);
|
||||
if (!match) return null;
|
||||
return { name: match[1], alias: match[2], order };
|
||||
}
|
||||
|
||||
function parseModuleImport(token: string, order: number): ParsedPythonImport | null {
|
||||
const cleaned = stripInlineComment(token).replace(/,$/, "").trim();
|
||||
if (cleaned.length === 0) return null;
|
||||
const match = /^([A-Za-z_][\w.]*)(?:\s+as\s+([A-Za-z_][\w]*))?$/.exec(cleaned);
|
||||
if (!match) return null;
|
||||
return {
|
||||
raw: `import ${cleaned}`,
|
||||
source: match[1],
|
||||
names: [],
|
||||
parsedNames: [],
|
||||
alias: match[2],
|
||||
kind: "module",
|
||||
group: classifyGroup(match[1]),
|
||||
order,
|
||||
multiline: false,
|
||||
startLine: 0,
|
||||
endLine: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function parseImportStatement(
|
||||
line: string,
|
||||
lineNumber: number,
|
||||
startOrder: number,
|
||||
): { records: ParsedPythonImport[]; nextOrder: number } | null {
|
||||
const match = /^import\s+(.+)$/.exec(stripInlineComment(line.trim()));
|
||||
if (!match) return null;
|
||||
let nextOrder = startOrder;
|
||||
const records: ParsedPythonImport[] = [];
|
||||
for (const token of match[1].split(",")) {
|
||||
const parsed = parseModuleImport(token, nextOrder);
|
||||
if (!parsed) continue;
|
||||
parsed.startLine = lineNumber;
|
||||
parsed.endLine = lineNumber;
|
||||
parsed.raw = line;
|
||||
records.push(parsed);
|
||||
nextOrder += 1;
|
||||
}
|
||||
return records.length === 0 ? null : { records, nextOrder };
|
||||
}
|
||||
|
||||
function countParentheses(text: string): number {
|
||||
let balance = 0;
|
||||
for (const char of text) {
|
||||
if (char === "(") balance += 1;
|
||||
if (char === ")") balance -= 1;
|
||||
}
|
||||
return balance;
|
||||
}
|
||||
|
||||
function parseFromStatement(
|
||||
lines: string[],
|
||||
startIndex: number,
|
||||
eol: string,
|
||||
startOrder: number,
|
||||
): { record: ParsedPythonImport; nextIndex: number; nextOrder: number } | null {
|
||||
const collected: string[] = [];
|
||||
let nextIndex = startIndex;
|
||||
let balance = 0;
|
||||
let sawParenthesis = false;
|
||||
while (nextIndex < lines.length) {
|
||||
const line = lines[nextIndex] ?? "";
|
||||
collected.push(line);
|
||||
const withoutComment = stripInlineComment(line);
|
||||
balance += countParentheses(withoutComment);
|
||||
sawParenthesis ||= withoutComment.includes("(");
|
||||
nextIndex += 1;
|
||||
if (sawParenthesis) {
|
||||
if (balance <= 0) break;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
const normalized = collected
|
||||
.map(line => stripInlineComment(line).trim())
|
||||
.join(" ")
|
||||
.replace(/\s+/g, " ");
|
||||
const match = /^from\s+([.A-Za-z_][\w.]*)\s+import\s+(.+)$/.exec(normalized);
|
||||
if (!match) return null;
|
||||
let namesPart = match[2].trim();
|
||||
if (namesPart.startsWith("(") && namesPart.endsWith(")")) {
|
||||
namesPart = namesPart.slice(1, -1).trim();
|
||||
}
|
||||
const parsedNames: ImportedName[] = [];
|
||||
let nextOrder = startOrder;
|
||||
for (const token of namesPart.split(",")) {
|
||||
const parsedName = parseImportedName(token, nextOrder);
|
||||
if (!parsedName) continue;
|
||||
parsedNames.push(parsedName);
|
||||
nextOrder += 1;
|
||||
}
|
||||
if (parsedNames.length === 0) return null;
|
||||
const record: ParsedPythonImport = {
|
||||
raw: collected.join(eol),
|
||||
source: match[1],
|
||||
names: parsedNames.map(formatImportedName),
|
||||
parsedNames,
|
||||
kind: "from",
|
||||
group: classifyGroup(match[1]),
|
||||
order: startOrder,
|
||||
multiline: sawParenthesis || collected.length > 1,
|
||||
startLine: startIndex + 1,
|
||||
endLine: nextIndex,
|
||||
};
|
||||
return { record, nextIndex, nextOrder };
|
||||
}
|
||||
|
||||
function compareImportedNames(left: ImportedName, right: ImportedName): number {
|
||||
const leftKey = `${left.name}|${left.alias ?? ""}`;
|
||||
const rightKey = `${right.name}|${right.alias ?? ""}`;
|
||||
return leftKey.localeCompare(rightKey);
|
||||
}
|
||||
|
||||
function compareRecords(left: ParsedPythonImport, right: ParsedPythonImport): number {
|
||||
const leftKey = left.kind === "module" ? `import ${left.source} ${left.alias ?? ""}` : `from ${left.source}`;
|
||||
const rightKey = right.kind === "module" ? `import ${right.source} ${right.alias ?? ""}` : `from ${right.source}`;
|
||||
return leftKey.localeCompare(rightKey);
|
||||
}
|
||||
|
||||
function hasBlankLineBetween(lines: string[], previousEndLine: number, nextStartLine: number): boolean {
|
||||
for (let index = previousEndLine; index < nextStartLine - 1; index += 1) {
|
||||
if (isBlankLine(lines[index] ?? "")) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function inferSorted(records: ParsedPythonImport[]): boolean {
|
||||
const byGroup = new Map<ImportGroup, ParsedPythonImport[]>();
|
||||
for (const record of records) {
|
||||
const groupRecords = byGroup.get(record.group);
|
||||
if (groupRecords) groupRecords.push(record);
|
||||
else byGroup.set(record.group, [record]);
|
||||
if (record.kind === "from") {
|
||||
for (let index = 1; index < record.parsedNames.length; index += 1) {
|
||||
if (compareImportedNames(record.parsedNames[index - 1]!, record.parsedNames[index]!) > 0) return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const group of DEFAULT_GROUP_ORDER) {
|
||||
const groupRecords = byGroup.get(group) ?? [];
|
||||
for (let index = 1; index < groupRecords.length; index += 1) {
|
||||
if (compareRecords(groupRecords[index - 1]!, groupRecords[index]!) > 0) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function inferStyle(records: ParsedPythonImport[], lines: string[]): PythonImportStyle {
|
||||
const groupOrder: ImportGroup[] = [];
|
||||
let groupSeparator = false;
|
||||
for (let index = 0; index < records.length; index += 1) {
|
||||
const record = records[index]!;
|
||||
if (!groupOrder.includes(record.group)) groupOrder.push(record.group);
|
||||
if (index > 0) {
|
||||
const previous = records[index - 1]!;
|
||||
if (hasBlankLineBetween(lines, previous.endLine, record.startLine)) groupSeparator = true;
|
||||
}
|
||||
}
|
||||
return {
|
||||
...defaultImportStyle,
|
||||
groupSeparator,
|
||||
groupOrder: groupOrder.length > 0 ? groupOrder : [...DEFAULT_GROUP_ORDER],
|
||||
sorted: inferSorted(records),
|
||||
parenthesized: records.some(record => record.kind === "from" && record.multiline),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeRecords(records: ParsedPythonImport[]): ParsedPythonImport[] {
|
||||
const moduleRecords = new Map<string, ParsedPythonImport>();
|
||||
const fromRecords = new Map<string, ParsedPythonImport>();
|
||||
for (const record of [...records].sort((left, right) => left.order - right.order)) {
|
||||
if (record.kind === "module") {
|
||||
const key = `${record.source}|${record.alias ?? ""}`;
|
||||
if (!moduleRecords.has(key)) moduleRecords.set(key, { ...record, parsedNames: [], names: [] });
|
||||
continue;
|
||||
}
|
||||
const existing = fromRecords.get(record.source);
|
||||
if (!existing) {
|
||||
fromRecords.set(record.source, {
|
||||
...record,
|
||||
parsedNames: [...record.parsedNames],
|
||||
names: [...record.names],
|
||||
});
|
||||
continue;
|
||||
}
|
||||
for (const name of record.parsedNames) {
|
||||
const alreadyPresent = existing.parsedNames.some(
|
||||
existingName => existingName.name === name.name && (existingName.alias ?? "") === (name.alias ?? ""),
|
||||
);
|
||||
if (alreadyPresent) continue;
|
||||
existing.parsedNames.push({ ...name, order: existing.parsedNames.length });
|
||||
}
|
||||
existing.names = existing.parsedNames.map(formatImportedName);
|
||||
existing.multiline ||= record.multiline;
|
||||
if (record.order < existing.order) existing.order = record.order;
|
||||
}
|
||||
const combined = [...moduleRecords.values(), ...fromRecords.values()];
|
||||
return combined.sort((left, right) => left.order - right.order);
|
||||
}
|
||||
|
||||
function renderRecord(record: ParsedPythonImport, style: PythonImportStyle): string {
|
||||
if (record.kind === "module") {
|
||||
return record.alias ? `import ${record.source} as ${record.alias}` : `import ${record.source}`;
|
||||
}
|
||||
const names = [...record.parsedNames];
|
||||
if (style.sorted) names.sort(compareImportedNames);
|
||||
const renderedNames = names.map(formatImportedName);
|
||||
const useParenthesized = record.multiline || (style.parenthesized && renderedNames.length > 1);
|
||||
if (!useParenthesized) {
|
||||
return `from ${record.source} import ${renderedNames.join(", ")}`;
|
||||
}
|
||||
return [`from ${record.source} import (`, ...renderedNames.map(name => ` ${name},`), `)`].join("\n");
|
||||
}
|
||||
|
||||
function groupComparator(left: ImportGroup, right: ImportGroup, style: PythonImportStyle): number {
|
||||
const orderedGroups = [
|
||||
...style.groupOrder,
|
||||
...DEFAULT_GROUP_ORDER.filter(group => !style.groupOrder.includes(group)),
|
||||
];
|
||||
return orderedGroups.indexOf(left) - orderedGroups.indexOf(right);
|
||||
}
|
||||
|
||||
function renderImportBlock(records: ParsedPythonImport[], style: PythonImportStyle): string[] {
|
||||
const normalized = normalizeRecords(records).map(record => ({
|
||||
...record,
|
||||
parsedNames: [...record.parsedNames],
|
||||
names: [...record.names],
|
||||
}));
|
||||
const grouped = new Map<ImportGroup, ParsedPythonImport[]>();
|
||||
for (const record of normalized) {
|
||||
const groupRecords = grouped.get(record.group);
|
||||
if (groupRecords) groupRecords.push(record);
|
||||
else grouped.set(record.group, [record]);
|
||||
}
|
||||
const orderedGroups = [...grouped.keys()].sort((left, right) => groupComparator(left, right, style));
|
||||
const renderedLines: string[] = [];
|
||||
for (const group of orderedGroups) {
|
||||
const groupRecords = grouped.get(group) ?? [];
|
||||
const orderedRecords = style.sorted
|
||||
? [...groupRecords].sort(compareRecords)
|
||||
: [...groupRecords].sort((left, right) => left.order - right.order);
|
||||
if (renderedLines.length > 0 && style.groupSeparator) renderedLines.push("");
|
||||
for (const record of orderedRecords) {
|
||||
renderedLines.push(...renderRecord(record, style).split("\n"));
|
||||
}
|
||||
}
|
||||
return renderedLines;
|
||||
}
|
||||
|
||||
function specToRecords(spec: ImportSpec, order: number): { records: ParsedPythonImport[]; warnings: string[] } {
|
||||
const warnings: string[] = [];
|
||||
if (spec.default) warnings.push(`Python imports do not support default import syntax for ${spec.from}`);
|
||||
if (spec.namespace) warnings.push(`Python imports do not support namespace import syntax for ${spec.from}`);
|
||||
if (spec.imports && spec.imports.length > 0) {
|
||||
const parsedNames = spec.imports.flatMap((item, index) => {
|
||||
const parsed = parseImportedName(item, order + index);
|
||||
if (parsed) return [parsed];
|
||||
return [];
|
||||
});
|
||||
if (parsedNames.length === 1 && spec.alias) parsedNames[0] = { ...parsedNames[0]!, alias: spec.alias };
|
||||
if (parsedNames.length === 0) return { records: [], warnings };
|
||||
return {
|
||||
records: [
|
||||
{
|
||||
raw: "",
|
||||
source: spec.from,
|
||||
names: parsedNames.map(formatImportedName),
|
||||
parsedNames,
|
||||
kind: "from",
|
||||
group: classifyGroup(spec.from),
|
||||
order,
|
||||
multiline: false,
|
||||
startLine: 0,
|
||||
endLine: 0,
|
||||
},
|
||||
],
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
return {
|
||||
records: [
|
||||
{
|
||||
raw: "",
|
||||
source: spec.from,
|
||||
names: [],
|
||||
parsedNames: [],
|
||||
alias: spec.alias,
|
||||
kind: "module",
|
||||
group: classifyGroup(spec.from),
|
||||
order,
|
||||
multiline: false,
|
||||
startLine: 0,
|
||||
endLine: 0,
|
||||
},
|
||||
],
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
function parsePythonRegion(content: string): ParsedPythonRegion | null {
|
||||
const lines = content.split(/\r?\n/);
|
||||
const eol = detectEol(content);
|
||||
const prefixEnd = findPrefixEnd(lines);
|
||||
let index = prefixEnd;
|
||||
while (index < lines.length && (isBlankLine(lines[index] ?? "") || isCommentLine(lines[index] ?? ""))) index += 1;
|
||||
if (!isImportStart(lines[index] ?? "")) return null;
|
||||
const records: ParsedPythonImport[] = [];
|
||||
let nextOrder = 0;
|
||||
while (index < lines.length) {
|
||||
const currentLine = lines[index] ?? "";
|
||||
if (isBlankLine(currentLine) || isCommentLine(currentLine)) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (currentLine.trimStart().startsWith("import ")) {
|
||||
const parsed = parseImportStatement(currentLine, index + 1, nextOrder);
|
||||
if (!parsed) break;
|
||||
records.push(...parsed.records);
|
||||
nextOrder = parsed.nextOrder;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (currentLine.trimStart().startsWith("from ")) {
|
||||
const parsed = parseFromStatement(lines, index, eol, nextOrder);
|
||||
if (!parsed) break;
|
||||
records.push(parsed.record);
|
||||
nextOrder = parsed.nextOrder;
|
||||
index = parsed.nextIndex;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (records.length === 0) return null;
|
||||
const normalizedRecords = normalizeRecords(records);
|
||||
return {
|
||||
startLine: normalizedRecords[0]!.startLine,
|
||||
endLine: Math.max(...normalizedRecords.map(record => record.endLine)),
|
||||
imports: normalizedRecords,
|
||||
style: inferStyle(normalizedRecords, lines),
|
||||
};
|
||||
}
|
||||
|
||||
function parseExisting(content: string): ImportRegion | null {
|
||||
return parsePythonRegion(content);
|
||||
}
|
||||
|
||||
function specToLine(spec: ImportSpec, style: ImportStyle): string {
|
||||
const pythonStyle = getPythonStyle(style);
|
||||
const { records } = specToRecords(spec, 0);
|
||||
return records.length === 0 ? "" : renderRecord(records[0]!, pythonStyle);
|
||||
}
|
||||
|
||||
function mergeRecords(
|
||||
existing: ParsedPythonImport[],
|
||||
requested: ParsedPythonImport[],
|
||||
style: PythonImportStyle,
|
||||
): { records: ParsedPythonImport[]; added: string[] } {
|
||||
const records = normalizeRecords(existing).map(record => ({
|
||||
...record,
|
||||
parsedNames: [...record.parsedNames],
|
||||
names: [...record.names],
|
||||
}));
|
||||
let nextOrder = records.reduce((max, record) => Math.max(max, record.order), -1) + 1;
|
||||
const added: string[] = [];
|
||||
for (const record of requested) {
|
||||
if (record.kind === "module") {
|
||||
const exists = records.some(
|
||||
existingRecord =>
|
||||
existingRecord.kind === "module" &&
|
||||
existingRecord.source === record.source &&
|
||||
(existingRecord.alias ?? "") === (record.alias ?? ""),
|
||||
);
|
||||
if (exists) continue;
|
||||
records.push({ ...record, order: nextOrder, startLine: 0, endLine: 0 });
|
||||
added.push(renderRecord(record, style));
|
||||
nextOrder += 1;
|
||||
continue;
|
||||
}
|
||||
const existingRecord = records.find(candidate => candidate.kind === "from" && candidate.source === record.source);
|
||||
if (!existingRecord || existingRecord.kind !== "from") {
|
||||
records.push({ ...record, order: nextOrder, startLine: 0, endLine: 0 });
|
||||
added.push(renderRecord(record, style));
|
||||
nextOrder += 1;
|
||||
continue;
|
||||
}
|
||||
const missingNames = record.parsedNames.filter(
|
||||
name =>
|
||||
!existingRecord.parsedNames.some(
|
||||
existingName => existingName.name === name.name && (existingName.alias ?? "") === (name.alias ?? ""),
|
||||
),
|
||||
);
|
||||
if (missingNames.length === 0) continue;
|
||||
for (const name of missingNames) {
|
||||
existingRecord.parsedNames.push({ ...name, order: existingRecord.parsedNames.length });
|
||||
}
|
||||
if (style.sorted) existingRecord.parsedNames.sort(compareImportedNames);
|
||||
existingRecord.names = existingRecord.parsedNames.map(formatImportedName);
|
||||
existingRecord.multiline ||= record.multiline;
|
||||
added.push(
|
||||
renderRecord(
|
||||
{ ...existingRecord, parsedNames: missingNames, names: missingNames.map(formatImportedName) },
|
||||
style,
|
||||
),
|
||||
);
|
||||
}
|
||||
return { records, added };
|
||||
}
|
||||
|
||||
function insertWithoutRegion(content: string, lines: string[], blockLines: string[], eol: string): string {
|
||||
if (content.length === 0) return blockLines.join(eol);
|
||||
const insertAt = findPrefixEnd(lines);
|
||||
const before = lines.slice(0, insertAt);
|
||||
const after = lines.slice(insertAt);
|
||||
const needsTrailingBlank =
|
||||
after.length > 0 && after.some(line => line.trim().length > 0) && !isBlankLine(after[0] ?? "");
|
||||
const inserted = [...before, ...blockLines, ...(needsTrailingBlank ? [""] : []), ...after];
|
||||
const nextContent = inserted.join(eol);
|
||||
if (content.endsWith(eol) && !nextContent.endsWith(eol)) return `${nextContent}${eol}`;
|
||||
return nextContent;
|
||||
}
|
||||
|
||||
function replaceRegion(
|
||||
content: string,
|
||||
lines: string[],
|
||||
region: ParsedPythonRegion,
|
||||
blockLines: string[],
|
||||
eol: string,
|
||||
): string {
|
||||
const nextLines = [...lines.slice(0, region.startLine - 1), ...blockLines, ...lines.slice(region.endLine)];
|
||||
const nextContent = nextLines.join(eol);
|
||||
if (content.endsWith(eol) && !nextContent.endsWith(eol)) return `${nextContent}${eol}`;
|
||||
return nextContent;
|
||||
}
|
||||
|
||||
export const pythonImportHandler: ImportHandler = {
|
||||
parseExisting,
|
||||
specToLine,
|
||||
apply(content, specs) {
|
||||
const region = parsePythonRegion(content);
|
||||
const style = getPythonStyle(
|
||||
region?.style ?? {
|
||||
...defaultImportStyle,
|
||||
groupSeparator: true,
|
||||
groupOrder: [...DEFAULT_GROUP_ORDER],
|
||||
sorted: true,
|
||||
parenthesized: false,
|
||||
},
|
||||
);
|
||||
const warnings: string[] = [];
|
||||
const requested: ParsedPythonImport[] = [];
|
||||
let order = 0;
|
||||
for (const spec of specs) {
|
||||
const result = specToRecords(spec, order);
|
||||
warnings.push(...result.warnings);
|
||||
requested.push(...result.records);
|
||||
order += Math.max(result.records.length, 1);
|
||||
}
|
||||
if (requested.length === 0) return { content, added: [], warnings };
|
||||
const { records, added } = mergeRecords(region?.imports ?? [], requested, style);
|
||||
if (added.length === 0) return { content, added: [], warnings };
|
||||
const eol = detectEol(content);
|
||||
const lines = content.split(/\r?\n/);
|
||||
const blockLines = renderImportBlock(records, style);
|
||||
const nextContent = region
|
||||
? replaceRegion(content, lines, region, blockLines, eol)
|
||||
: insertWithoutRegion(content, lines, blockLines, eol);
|
||||
return { content: nextContent, added, warnings };
|
||||
},
|
||||
};
|
||||
|
||||
export { defaultImportStyle };
|
||||
Reference in New Issue
Block a user