diff --git a/src/linter/linter.ts b/src/linter/linter.ts index 0d3dd8b0..973afa6a 100644 --- a/src/linter/linter.ts +++ b/src/linter/linter.ts @@ -188,26 +188,38 @@ async function getProjectGraph(rootDir: string, ui5Config?: string | object): Pr rootConfigPath = ui5YamlPath; } else { if (ui5Config) throw new Error(`Unable to find UI5 config file '${ui5Config}'`); - const isApp = await dirExists(path.join(rootDir, "webapp")); - if (isApp) { - rootConfiguration = { - specVersion: "3.0", - type: "application", - metadata: { - name: "ui5-linter-target", - }, - }; - } else { - const isLibrary = await dirExists(path.join(rootDir, "src")); - if (isLibrary) { - rootConfiguration = { - specVersion: "3.0", - type: "library", - metadata: { - name: "ui5-linter-target", - }, - }; - } + + const dirChecks = await Promise.all([ + dirExists(path.join(rootDir, "webapp")), + dirExists(path.join(rootDir, "src", "main", "webapp")), + dirExists(path.join(rootDir, "WebContent")), + dirExists(path.join(rootDir, "src", "main", "jslib")), + dirExists(path.join(rootDir, "src", "main", "js")), + dirExists(path.join(rootDir, "src", "main", "uilib")), + dirExists(path.join(rootDir, "src")), + ]); + + if (dirChecks[0]) { + // Common app with webapp folder + rootConfiguration = createProjectConfig("application", "webapp"); + } else if (dirChecks[1]) { + // Legacy app with src/main/webapp folder + rootConfiguration = createProjectConfig("application", "src/main/webapp"); + } else if (dirChecks[2]) { + // Legacy app with WebContent folder + rootConfiguration = createProjectConfig("application", "WebContent"); + } else if (dirChecks[3]) { + // Library with src/main/jslib folder + rootConfiguration = createProjectConfig("library", "src/main/jslib", "src/test/jslib"); + } else if (dirChecks[4]) { + // Library with src/main/js folder + rootConfiguration = createProjectConfig("library", "src/main/js", "src/test/js"); + } else if (dirChecks[5]) { + // Library with src/main/uilib folder + rootConfiguration = createProjectConfig("library", "src/main/uilib", "src/test/uilib"); + } else if (dirChecks[6]) { + // Library with src folder + rootConfiguration = createProjectConfig("library", "src", "test"); } } @@ -231,6 +243,47 @@ async function getProjectGraph(rootDir: string, ui5Config?: string | object): Pr }); } +interface ProjectConfig { + specVersion: string; + type: string; + metadata: { + name: string; + }; + resources?: { + configuration: { + paths: { + webapp?: string; + src?: string; + test?: string; + }; + }; + }; +} + +function createProjectConfig(projectType: string, projectSrcPath?: string, projectTestPath?: string): ProjectConfig { + let resourcesConfig: ProjectConfig["resources"] = { + configuration: { + paths: {}, + }, + }; + if (projectType === "application") { + resourcesConfig.configuration.paths.webapp = projectSrcPath ?? "webapp"; + } else if (projectType === "library") { + resourcesConfig.configuration.paths.src = projectSrcPath ?? "src"; + resourcesConfig.configuration.paths.test = projectTestPath ?? "test"; + } else { + // Do not set a resources configuration for other project types + resourcesConfig = undefined; + } + return { + specVersion: "4.0", + type: projectType, + metadata: { + name: "ui5-linter-project", + }, + resources: resourcesConfig, + }; +} /** * Normalize provided virtual paths to the original file paths */ @@ -389,3 +442,8 @@ export function mergeIgnorePatterns(options: LinterOptions, config: UI5LintConfi ...(options.ignorePatterns ?? []), // CLI patterns go after config patterns ].filter(($) => $); } + +// Export local function for testing only +export const __localFunctions__ = (process.env.NODE_ENV === "test") ? + {getProjectGraph} : + /* istanbul ignore next */ undefined; diff --git a/test/fixtures/linter/projects/legacy-dirs/legacy.app.a/WebContent/manifest.json b/test/fixtures/linter/projects/legacy-dirs/legacy.app.a/WebContent/manifest.json new file mode 100644 index 00000000..3ff5a622 --- /dev/null +++ b/test/fixtures/linter/projects/legacy-dirs/legacy.app.a/WebContent/manifest.json @@ -0,0 +1,7 @@ +{ + "_version": "1.12.0", + "sap.app": { + "id": "com.app", + "type": "application" + } +} diff --git a/test/fixtures/linter/projects/legacy-dirs/legacy.app.b/src/main/webapp/manifest.json b/test/fixtures/linter/projects/legacy-dirs/legacy.app.b/src/main/webapp/manifest.json new file mode 100644 index 00000000..3ff5a622 --- /dev/null +++ b/test/fixtures/linter/projects/legacy-dirs/legacy.app.b/src/main/webapp/manifest.json @@ -0,0 +1,7 @@ +{ + "_version": "1.12.0", + "sap.app": { + "id": "com.app", + "type": "application" + } +} diff --git a/test/fixtures/linter/projects/legacy-dirs/legacy.lib.a/src/main/jslib/.library b/test/fixtures/linter/projects/legacy-dirs/legacy.lib.a/src/main/jslib/.library new file mode 100644 index 00000000..61286d35 --- /dev/null +++ b/test/fixtures/linter/projects/legacy-dirs/legacy.lib.a/src/main/jslib/.library @@ -0,0 +1,7 @@ + + + library.with.custom.paths + SAP SE + ${version} + ${copyright} + diff --git a/test/fixtures/linter/projects/legacy-dirs/legacy.lib.b/src/main/uilib/.library b/test/fixtures/linter/projects/legacy-dirs/legacy.lib.b/src/main/uilib/.library new file mode 100644 index 00000000..61286d35 --- /dev/null +++ b/test/fixtures/linter/projects/legacy-dirs/legacy.lib.b/src/main/uilib/.library @@ -0,0 +1,7 @@ + + + library.with.custom.paths + SAP SE + ${version} + ${copyright} + diff --git a/test/fixtures/linter/projects/legacy-dirs/legacy.lib.c/src/main/js/.library b/test/fixtures/linter/projects/legacy-dirs/legacy.lib.c/src/main/js/.library new file mode 100644 index 00000000..61286d35 --- /dev/null +++ b/test/fixtures/linter/projects/legacy-dirs/legacy.lib.c/src/main/js/.library @@ -0,0 +1,7 @@ + + + library.with.custom.paths + SAP SE + ${version} + ${copyright} + diff --git a/test/lib/linter/linter.ts b/test/lib/linter/linter.ts index dafbd754..1fd717b8 100644 --- a/test/lib/linter/linter.ts +++ b/test/lib/linter/linter.ts @@ -2,6 +2,7 @@ import anyTest, {TestFn} from "ava"; import sinonGlobal, {SinonStub} from "sinon"; import path from "node:path"; import {fileURLToPath} from "node:url"; +import esmock from "esmock"; import { createTestsForFixtures, assertExpectedLintResults, esmockDeprecationText, preprocessLintResultsForSnapshot, @@ -28,7 +29,7 @@ test.after.always((t) => { t.context.sinon.restore(); }); -// Define tests for reach file in the fixtures/linter/general directory +// Define tests for each file in the fixtures/linter/general directory createTestsForFixtures(fixturesGeneralPath); // Test project fixtures individually @@ -272,3 +273,154 @@ test.serial("lint: com.ui5.troublesome.app with custom UI5 config which does NOT ui5Config, }), {message: `Unable to find UI5 config file '${ui5Config}'`}); }); + +test.serial("lint: getProjectGraph with different directory structures", async (t) => { + const graphFromObjectStub = t.context.sinon.stub().resolves(); + const {__localFunctions__} = await esmock("../../../src/linter/linter.js", { + "@ui5/project/graph": { + graphFromObject: graphFromObjectStub, + }, + }); + + const {getProjectGraph} = __localFunctions__; + + const basePath = path.join(fixturesProjectsPath, "legacy-dirs"); + + // Legacy app structures + const appA = path.join(basePath, "legacy.app.a"); + const appB = path.join(basePath, "legacy.app.b"); + const libA = path.join(basePath, "legacy.lib.a"); + const libB = path.join(basePath, "legacy.lib.b"); + const libC = path.join(basePath, "legacy.lib.c"); + + await getProjectGraph(appA); + await getProjectGraph(appB); + await getProjectGraph(libA); + await getProjectGraph(libB); + await getProjectGraph(libC); + + t.is(graphFromObjectStub.callCount, 5); + t.deepEqual(graphFromObjectStub.getCall(0).args[0], { + dependencyTree: { + dependencies: [], + id: "ui5-linter-target", + path: appA, + version: "1.0.0", + }, + resolveFrameworkDependencies: false, + rootConfigPath: undefined, + rootConfiguration: { + metadata: { + name: "ui5-linter-project", + }, + resources: { + configuration: { + paths: { + webapp: "WebContent", + }, + }, + }, + specVersion: "4.0", + type: "application", + }, + }); + t.deepEqual(graphFromObjectStub.getCall(1).args[0], { + dependencyTree: { + dependencies: [], + id: "ui5-linter-target", + path: appB, + version: "1.0.0", + }, + resolveFrameworkDependencies: false, + rootConfigPath: undefined, + rootConfiguration: { + metadata: { + name: "ui5-linter-project", + }, + resources: { + configuration: { + paths: { + webapp: "src/main/webapp", + }, + }, + }, + specVersion: "4.0", + type: "application", + }, + }); + t.deepEqual(graphFromObjectStub.getCall(2).args[0], { + dependencyTree: { + dependencies: [], + id: "ui5-linter-target", + path: libA, + version: "1.0.0", + }, + resolveFrameworkDependencies: false, + rootConfigPath: undefined, + rootConfiguration: { + metadata: { + name: "ui5-linter-project", + }, + resources: { + configuration: { + paths: { + src: "src/main/jslib", + test: "src/test/jslib", + }, + }, + }, + specVersion: "4.0", + type: "library", + }, + }); + t.deepEqual(graphFromObjectStub.getCall(3).args[0], { + dependencyTree: { + dependencies: [], + id: "ui5-linter-target", + path: libB, + version: "1.0.0", + }, + resolveFrameworkDependencies: false, + rootConfigPath: undefined, + rootConfiguration: { + metadata: { + name: "ui5-linter-project", + }, + resources: { + configuration: { + paths: { + src: "src/main/uilib", + test: "src/test/uilib", + }, + }, + }, + specVersion: "4.0", + type: "library", + }, + }); + t.deepEqual(graphFromObjectStub.getCall(4).args[0], { + dependencyTree: { + dependencies: [], + id: "ui5-linter-target", + path: libC, + version: "1.0.0", + }, + resolveFrameworkDependencies: false, + rootConfigPath: undefined, + rootConfiguration: { + metadata: { + name: "ui5-linter-project", + }, + resources: { + configuration: { + paths: { + src: "src/main/js", + test: "src/test/js", + }, + }, + }, + specVersion: "4.0", + type: "library", + }, + }); +});