/* * Copyright 2025 the original author or authors. *

* Licensed under the Moderne Source Available License (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *

* https://docs.moderne.io/licensing/moderne-source-available-license *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import * as rpc from "vscode-jsonrpc/node"; import * as path from "path"; import {ExecutionContext} from "../../execution"; import {randomId, UUID} from "../../uuid"; import {SourceFile} from "../../tree"; import {Parsers} from "../../parser"; import {withMetrics} from "./metrics"; import {replaceMarkerByKind} from "../../markers"; import {setLastParsedProject} from "./last-parsed-project"; /** * Response item with object ID and source file type for proper deserialization. */ export interface ParseProjectResponseItem { id: UUID; sourceFileType: string; /** * Relative source path of the discovered file. Used by the Java side to report * per-file failures (e.g., a failed `GetObject` deserialization) without aborting * the rest of the stream. Optional for older callers. */ sourcePath?: string; } /** * RPC request to parse an entire project directory. * Discovers and parses: * - JavaScript/TypeScript source files * - package.json files (with NodeResolutionResult markers) * - Lock files (package-lock.json as JSON, yarn.lock/pnpm-lock.yaml as YAML) * * Uses ProjectParser for file discovery and Prettier detection. */ export class ParseProject { constructor( private readonly projectPath: string, private readonly exclusions?: string[], /** * Optional path to make source file paths relative to. * If not specified, paths are relative to projectPath. * Use this when parsing a subdirectory but wanting paths relative to the repository root. */ private readonly relativeTo?: string ) {} static handle( connection: rpc.MessageConnection, localObjects: Map any) | any>, metricsCsv?: string ): void { connection.onRequest( new rpc.RequestType("ParseProject"), withMetrics( "ParseProject", metricsCsv, (context) => async (request) => { context.target = request.projectPath; // Dynamic import to break circular dependency const {ProjectParser} = await import("../../javascript/index.js"); const projectPath = path.resolve(request.projectPath); setLastParsedProject(projectPath); // Use relativeTo if specified, otherwise default to projectPath const relativeTo = request.relativeTo ? path.resolve(request.relativeTo) : projectPath; // Undefined when unset, so the parser can match exclusions to its discovery mode. const projectParser = new ProjectParser(projectPath, {exclusions: request.exclusions}); const discovered = await projectParser.discoverFiles(); const prettierLoader = await projectParser.createPrettierLoader(); const resultItems: ParseProjectResponseItem[] = []; const ctx = new ExecutionContext(); // Parse package.json files (these get NodeResolutionResult markers) if (discovered.packageJsonFiles.length > 0) { const parser = Parsers.createParser("packageJson", { ctx, relativeTo }); const generator = parser.parse(...discovered.packageJsonFiles); for (const filePath of discovered.packageJsonFiles) { const id = randomId(); localObjects.set(id, async (id: string) => { const sourceFile: SourceFile = (await generator.next()).value; return { ...sourceFile, id }; }); resultItems.push({ id, sourceFileType: "org.openrewrite.json.tree.Json$Document", // break cycle sourcePath: path.relative(relativeTo, filePath) }); } } // Parse JSON lock files if (discovered.lockFiles.json.length > 0) { const parser = Parsers.createParser("json", {ctx, relativeTo}); const generator = parser.parse(...discovered.lockFiles.json); for (const filePath of discovered.lockFiles.json) { const id = randomId(); localObjects.set(id, async (id: string) => { const sourceFile: SourceFile = (await generator.next()).value; return { ...sourceFile, id }; }); resultItems.push({ id, sourceFileType: "org.openrewrite.json.tree.Json$Document", // break cycle sourcePath: path.relative(relativeTo, filePath) }); } } // Parse YAML lock files if (discovered.lockFiles.yaml.length > 0) { const parser = Parsers.createParser("yaml", {ctx, relativeTo}); const generator = parser.parse(...discovered.lockFiles.yaml); for (const filePath of discovered.lockFiles.yaml) { const id = randomId(); localObjects.set(id, async (id: string) => { const sourceFile: SourceFile = (await generator.next()).value; return { ...sourceFile, id }; }); resultItems.push({ id, sourceFileType: "org.openrewrite.yaml.tree.Yaml$Documents", // break cycle sourcePath: path.relative(relativeTo, filePath) }); } } // Parse text lock files (yarn.lock Classic) if (discovered.lockFiles.text.length > 0) { const parser = Parsers.createParser("plainText", {ctx, relativeTo}); const generator = parser.parse(...discovered.lockFiles.text); for (const filePath of discovered.lockFiles.text) { const id = randomId(); localObjects.set(id, async (id: string) => { const sourceFile: SourceFile = (await generator.next()).value; return { ...sourceFile, id }; }); resultItems.push({ id, sourceFileType: "org.openrewrite.text.PlainText", // break cycle sourcePath: path.relative(relativeTo, filePath) }); } } // Parse JavaScript/TypeScript source files if (discovered.jsFiles.length > 0) { const parser = Parsers.createParser("javascript", { ctx, relativeTo }); // Check if Prettier is available const detection = await prettierLoader.detectPrettier(); if (detection.available) { // Prettier is available: add per-file PrettierStyle markers const generator = parser.parse(...discovered.jsFiles); for (const filePath of discovered.jsFiles) { const id = randomId(); localObjects.set(id, async (id: string) => { const sourceFile: SourceFile = (await generator.next()).value; // Add PrettierStyle marker if Prettier is available const prettierMarker = await prettierLoader.getConfigMarker(filePath); return { ...sourceFile, id, markers: prettierMarker ? replaceMarkerByKind(sourceFile.markers, prettierMarker) : sourceFile.markers }; }); resultItems.push({ id, sourceFileType: "org.openrewrite.javascript.tree.JS$CompilationUnit", // break cycle sourcePath: path.relative(relativeTo, filePath) }); } } else { // Prettier is NOT available: auto-detect styles from parsed files // Parse all files first to sample them const parsedFiles: {id: string, sourceFile: SourceFile, filePath: string}[] = []; let fileIndex = 0; for await (const sourceFile of parser.parse(...discovered.jsFiles)) { const id = randomId(); parsedFiles.push({id, sourceFile, filePath: discovered.jsFiles[fileIndex++]}); } // Sample all parsed files and build Autodetect marker using ProjectParser helper const autodetectMarker = await projectParser.buildAutodetectMarker( parsedFiles.map(p => p.sourceFile) ); // Store thunks that add the Autodetect marker for (const {id, sourceFile, filePath} of parsedFiles) { localObjects.set(id, async (newId: string) => { return { ...sourceFile, id: newId, markers: replaceMarkerByKind(sourceFile.markers, autodetectMarker) }; }); resultItems.push({ id, sourceFileType: "org.openrewrite.javascript.tree.JS$CompilationUnit", // break cycle sourcePath: path.relative(relativeTo, filePath) }); } } } // Unparseable JS/TS files (currently: too large) are recorded as // Quarks. The Java side builds the Quark locally from sourcePath, // so no GetObject round-trip and no content crosses the wire. for (const filePath of discovered.unparseableFiles) { resultItems.push({ id: randomId(), sourceFileType: "org.openrewrite.quark.Quark", // break cycle sourcePath: path.relative(relativeTo, filePath) }); } // Parse other YAML files if (discovered.yamlFiles.length > 0) { const parser = Parsers.createParser("yaml", {ctx, relativeTo}); const generator = parser.parse(...discovered.yamlFiles); for (const filePath of discovered.yamlFiles) { const id = randomId(); localObjects.set(id, async (id: string) => { const sourceFile: SourceFile = (await generator.next()).value; return { ...sourceFile, id }; }); resultItems.push({ id, sourceFileType: "org.openrewrite.yaml.tree.Yaml$Documents", // break cycle sourcePath: path.relative(relativeTo, filePath) }); } } // Parse other JSON files if (discovered.jsonFiles.length > 0) { const parser = Parsers.createParser("json", {ctx, relativeTo}); const generator = parser.parse(...discovered.jsonFiles); for (const filePath of discovered.jsonFiles) { const id = randomId(); localObjects.set(id, async (id: string) => { const sourceFile: SourceFile = (await generator.next()).value; return { ...sourceFile, id }; }); resultItems.push({ id, sourceFileType: "org.openrewrite.json.tree.Json$Document", // break cycle sourcePath: path.relative(relativeTo, filePath) }); } } // Parse text config files (.prettierignore, .gitignore, etc.) if (discovered.textFiles.length > 0) { const parser = Parsers.createParser("plainText", {ctx, relativeTo}); const generator = parser.parse(...discovered.textFiles); for (const filePath of discovered.textFiles) { const id = randomId(); localObjects.set(id, async (id: string) => { const sourceFile: SourceFile = (await generator.next()).value; return { ...sourceFile, id }; }); resultItems.push({ id, sourceFileType: "org.openrewrite.text.PlainText", // break cycle sourcePath: path.relative(relativeTo, filePath) }); } } return resultItems; } ) ); } }