Fix incorrect model files on case-insensitive file systems

This fixes some incorrect model files on case-insensitive file systems
when the package names are the same but the capitalization is different.

For example, when there are two packages `Volo.Abp.TestApp.MongoDb` and
`Volo.Abp.TestApp.MongoDB`, there would be 1 model file for each
package. However, on case-insensitive file systems, the second file
would overwrite the first file. This results in missing models. This
fixes it by canonicalizing the filenames to lowercase and writing all
files with the same package name to the same file.
This commit is contained in:
Koen Vlaswinkel
2023-12-15 15:06:22 +01:00
parent 60d777abf1
commit ef2f9d9c90
2 changed files with 170 additions and 11 deletions

View File

@@ -181,14 +181,23 @@ function createDataExtensionYamlsByGrouping(
>,
createFilename: (method: Method) => string,
): Record<string, string> {
const methodsByFilename: Record<string, Record<string, ModeledMethod[]>> = {};
const actualFilenameByCanonicalFilename: Record<string, string> = {};
const methodsByCanonicalFilename: Record<
string,
Record<string, ModeledMethod[]>
> = {};
// We only want to generate a yaml file when it's a known external API usage
// and there are new modeled methods for it. This avoids us overwriting other
// files that may contain data we don't know about.
for (const method of methods) {
if (method.signature in newModeledMethods) {
methodsByFilename[createFilename(method)] = {};
const filename = createFilename(method);
const canonicalFilename = canonicalizeFilename(filename);
methodsByCanonicalFilename[canonicalFilename] = {};
actualFilenameByCanonicalFilename[canonicalFilename] = filename;
}
}
@@ -196,10 +205,16 @@ function createDataExtensionYamlsByGrouping(
for (const [filename, methodsBySignature] of Object.entries(
existingModeledMethods,
)) {
if (filename in methodsByFilename) {
const canonicalFilename = canonicalizeFilename(filename);
if (canonicalFilename in methodsByCanonicalFilename) {
for (const [signature, methods] of Object.entries(methodsBySignature)) {
methodsByFilename[filename][signature] = [...methods];
methodsByCanonicalFilename[canonicalFilename][signature] = [...methods];
}
// Ensure that if a file exists on disk, we use the same capitalization
// as the original file.
actualFilenameByCanonicalFilename[canonicalFilename] = filename;
}
}
@@ -209,19 +224,29 @@ function createDataExtensionYamlsByGrouping(
const newMethods = newModeledMethods[method.signature];
if (newMethods) {
const filename = createFilename(method);
const canonicalFilename = canonicalizeFilename(filename);
// Override any existing modeled methods with the new ones.
methodsByFilename[filename][method.signature] = [...newMethods];
methodsByCanonicalFilename[canonicalFilename][method.signature] = [
...newMethods,
];
if (!(canonicalFilename in actualFilenameByCanonicalFilename)) {
actualFilenameByCanonicalFilename[canonicalFilename] = filename;
}
}
}
const result: Record<string, string> = {};
for (const [filename, methods] of Object.entries(methodsByFilename)) {
result[filename] = createDataExtensionYaml(
language,
Object.values(methods).flatMap((methods) => methods),
);
for (const [canonicalFilename, methods] of Object.entries(
methodsByCanonicalFilename,
)) {
result[actualFilenameByCanonicalFilename[canonicalFilename]] =
createDataExtensionYaml(
language,
Object.values(methods).flatMap((methods) => methods),
);
}
return result;
@@ -299,6 +324,13 @@ export function createFilenameForPackage(
return `${prefix}${packageName}${suffix}.yml`;
}
function canonicalizeFilename(filename: string) {
// We want to canonicalize filenames so that they are always in the same format
// for comparison purposes. This is important because we want to avoid overwriting
// data extension YAML files on case-insensitive file systems.
return filename.toLowerCase();
}
function validateModelExtensionFile(data: unknown): data is ModelExtensionFile {
modelExtensionFileSchemaValidate(data);

View File

@@ -6,8 +6,9 @@ import {
createFilenameForPackage,
loadDataExtensionYaml,
} from "../../../src/model-editor/yaml";
import { CallClassification } from "../../../src/model-editor/method";
import { CallClassification, Method } from "../../../src/model-editor/method";
import { QueryLanguage } from "../../../src/common/query-language";
import { ModeledMethod } from "../../../src/model-editor/modeled-method";
describe("createDataExtensionYaml", () => {
it("creates the correct YAML file", () => {
@@ -980,6 +981,132 @@ describe("createDataExtensionYamlsForFrameworkMode", () => {
`,
});
});
describe("with same package names but different capitalizations", () => {
const methods: Method[] = [
{
library: "HostTestAppDbContext",
signature:
"Volo.Abp.TestApp.MongoDb.HostTestAppDbContext#get_FifthDbContextDummyEntity()",
packageName: "Volo.Abp.TestApp.MongoDb",
typeName: "HostTestAppDbContext",
methodName: "get_FifthDbContextDummyEntity",
methodParameters: "()",
supported: false,
supportedType: "none",
usages: [],
},
{
library: "CityRepository",
signature:
"Volo.Abp.TestApp.MongoDB.CityRepository#FindByNameAsync(System.String)",
packageName: "Volo.Abp.TestApp.MongoDB",
typeName: "CityRepository",
methodName: "FindByNameAsync",
methodParameters: "(System.String)",
supported: false,
supportedType: "none",
usages: [],
},
];
const newModeledMethods: Record<string, ModeledMethod[]> = {
"Volo.Abp.TestApp.MongoDb.HostTestAppDbContext#get_FifthDbContextDummyEntity()":
[
{
type: "sink",
input: "Argument[0]",
kind: "sql",
provenance: "df-generated",
signature:
"Volo.Abp.TestApp.MongoDb.HostTestAppDbContext#get_FifthDbContextDummyEntity()",
packageName: "Volo.Abp.TestApp.MongoDb",
typeName: "HostTestAppDbContext",
methodName: "get_FifthDbContextDummyEntity",
methodParameters: "()",
},
],
"Volo.Abp.TestApp.MongoDB.CityRepository#FindByNameAsync(System.String)":
[
{
type: "neutral",
kind: "summary",
provenance: "df-generated",
signature:
"Volo.Abp.TestApp.MongoDB.CityRepository#FindByNameAsync(System.String)",
packageName: "Volo.Abp.TestApp.MongoDB",
typeName: "CityRepository",
methodName: "FindByNameAsync",
methodParameters: "(System.String)",
},
],
};
const modelYaml = `extensions:
- addsTo:
pack: codeql/csharp-all
extensible: sourceModel
data: []
- addsTo:
pack: codeql/csharp-all
extensible: sinkModel
data:
- ["Volo.Abp.TestApp.MongoDb","HostTestAppDbContext",true,"get_FifthDbContextDummyEntity","()","","Argument[0]","sql","df-generated"]
- addsTo:
pack: codeql/csharp-all
extensible: summaryModel
data: []
- addsTo:
pack: codeql/csharp-all
extensible: neutralModel
data:
- ["Volo.Abp.TestApp.MongoDB","CityRepository","FindByNameAsync","(System.String)","summary","df-generated"]
`;
it("creates the correct YAML files when there are existing modeled methods", () => {
const yaml = createDataExtensionYamlsForFrameworkMode(
QueryLanguage.CSharp,
methods,
newModeledMethods,
{},
);
expect(yaml).toEqual({
"models/Volo.Abp.TestApp.MongoDB.model.yml": modelYaml,
});
});
it("creates the correct YAML files when there are existing modeled methods", () => {
const yaml = createDataExtensionYamlsForFrameworkMode(
QueryLanguage.CSharp,
methods,
newModeledMethods,
{
"models/Volo.Abp.TestApp.mongodb.model.yml": {
"Volo.Abp.TestApp.MongoDB.CityRepository#FindByNameAsync(System.String)":
[
{
type: "neutral",
kind: "summary",
provenance: "manual",
signature:
"Volo.Abp.TestApp.MongoDB.CityRepository#FindByNameAsync(System.String)",
packageName: "Volo.Abp.TestApp.MongoDB",
typeName: "CityRepository",
methodName: "FindByNameAsync",
methodParameters: "(System.String)",
},
],
},
},
);
expect(yaml).toEqual({
"models/Volo.Abp.TestApp.mongodb.model.yml": modelYaml,
});
});
});
});
describe("loadDataExtensionYaml", () => {