Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Detect deprecations in ManagedObject metadata #349

Merged
merged 33 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
86a207b
test: Add test cases
d3xter666 Oct 7, 2024
9cde1b8
feat: Add more test cases
d3xter666 Oct 7, 2024
ce4760e
feat: Add scrap deprecations info from api.json
d3xter666 Oct 9, 2024
ffd75cd
feat: Lint deprecations in ManagedObject's metadata
d3xter666 Oct 9, 2024
e96ccd7
fix: Add also "fields" into the deprecations
d3xter666 Oct 9, 2024
856e32c
fix: Types & code
d3xter666 Oct 9, 2024
021d4bb
fix: Add api-deprecations data to the tests
d3xter666 Oct 9, 2024
4bd2a02
test: Update snapshots
d3xter666 Oct 9, 2024
d7ff23d
refactor: Limit types for the different cases
d3xter666 Oct 9, 2024
d81a045
refactor: Add deprecation text + since
d3xter666 Oct 9, 2024
055a79a
fix: Update details texts
d3xter666 Oct 9, 2024
441f023
feat: Add details info
d3xter666 Oct 9, 2024
1a23683
test: Update snapshots
d3xter666 Oct 9, 2024
1eeb6ac
fix: Resolve merge conflicts
d3xter666 Oct 10, 2024
0516899
refactor: Add specific messages
d3xter666 Oct 10, 2024
3b7515e
fix: Update snapshots
d3xter666 Oct 10, 2024
487bb77
fix: Determine real ancestors of ManagedObject
d3xter666 Oct 10, 2024
1386580
fix: Class extend transpilation detection
d3xter666 Oct 10, 2024
36a2384
test: Update texts
d3xter666 Oct 10, 2024
d480b97
fix: Add missing perf indicator
d3xter666 Oct 11, 2024
a96e9a8
refactor: Merge + api-extract.json + api-deprecations.json
d3xter666 Oct 11, 2024
eeb12ee
refactor: Reuse api-extract.json
d3xter666 Oct 11, 2024
fd857c2
test: Add case to inherit directly from ManagedObject
d3xter666 Oct 11, 2024
a313eed
feat: Analyze ManagedObject's metadata with own node visitor (#360)
d3xter666 Oct 15, 2024
7320975
fix: Test snapshots
d3xter666 Oct 15, 2024
0bce4c1
refactor: Remove redundant check
d3xter666 Oct 15, 2024
859a255
refactor: Strip complex type definitions
d3xter666 Oct 15, 2024
13854c5
docs: Update comments
d3xter666 Oct 15, 2024
2e51fd2
docs: Update comments
d3xter666 Oct 15, 2024
abaaacd
refactor: Remove redundant code + update test snapshots
d3xter666 Oct 15, 2024
e11d4f7
test: Add array param
d3xter666 Oct 15, 2024
eedc8b6
fix: Test formats
d3xter666 Oct 15, 2024
7d5dffb
test: Add var assignment + return
d3xter666 Oct 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,057 changes: 1,057 additions & 0 deletions resources/api-extract.json

Large diffs are not rendered by default.

45 changes: 44 additions & 1 deletion scripts/metadataProvider/createMetadataInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ import MetadataProvider from "./MetadataProvider.js";
import {
forEachSymbol,
} from "@ui5-language-assistant/semantic-model";
import {
UI5Class,
UI5Enum,
UI5Namespace,
UI5Interface,
UI5Typedef,
UI5Function,
UI5SemanticModel,
} from "@ui5-language-assistant/semantic-model-types";
type ui5UnionType = UI5Class | UI5Enum | UI5Namespace | UI5Interface | UI5Typedef | UI5Function;
const hasFieldsProperty = function (type: unknown): type is UI5Class | UI5Enum | UI5Namespace {
return (type as UI5Class | UI5Enum | UI5Namespace).fields !== undefined;
};

export default async function createMetadataInfo(apiJsonsRoot: string, sapui5Version: string) {
const metadataProvider = new MetadataProvider();
Expand All @@ -25,10 +38,40 @@ export default async function createMetadataInfo(apiJsonsRoot: string, sapui5Ver
version: sapui5Version,
},
defaultAggregations,
deprecations: createDeprecationsInfo(semanticModel),
};

await writeFile(
new URL("../../resources/api-extract.json", import.meta.url),
JSON.stringify(apiExtract, null, 2)
JSON.stringify(apiExtract, null, 2) + "\n"
);
}

function createDeprecationsInfo(semanticModel: UI5SemanticModel) {
const deprecations: Record<string, Record<string, string>> = {};
for (const [modelName, model] of Object.entries(semanticModel)) {
if (typeof model !== "object" || Array.isArray(model)) {
continue;
}

deprecations[modelName] = deprecations[modelName] ?? {};

for (const [typeName, type] of Object.entries(model as Record<string, ui5UnionType>)) {
if (type?.deprecatedInfo?.isDeprecated) {
deprecations[modelName][typeName] =
(type.deprecatedInfo.since ? `(since ${type.deprecatedInfo.since}) ` : "") +
(type.deprecatedInfo.text ? `${type.deprecatedInfo.text}` : "");
}

if (hasFieldsProperty(type)) {
type.fields?.forEach((field) => {
if (field?.deprecatedInfo?.isDeprecated) {
deprecations[modelName][typeName + "." + field.name] = "deprecated";
}
});
}
}
}

return deprecations;
}
20 changes: 20 additions & 0 deletions src/linter/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export enum MESSAGE {
DEPRECATED_API_ACCESS,
DEPRECATED_BOOTSTRAP_PARAM,
DEPRECATED_CLASS,
DEPRECATED_INTERFACE,
DEPRECATED_TYPE,
DEPRECATED_COMPONENT,
DEPRECATED_DECLARATIVE_SUPPORT,
DEPRECATED_FUNCTION_CALL,
Expand Down Expand Up @@ -140,6 +142,24 @@ export const MESSAGE_INFO = {
details: ({details}: {details: string}) => details,
},

[MESSAGE.DEPRECATED_INTERFACE]: {
severity: LintMessageSeverity.Error,
ruleId: RULES["no-deprecated-api"],

message: ({interfaceName}: {interfaceName: string}) =>
`Use of deprecated interface '${interfaceName}'`,
details: ({details}: {details: string}) => details,
},

[MESSAGE.DEPRECATED_TYPE]: {
severity: LintMessageSeverity.Error,
ruleId: RULES["no-deprecated-api"],

message: ({typeName}: {typeName: string}) =>
`Use of deprecated type '${typeName}'`,
details: ({details}: {details: string}) => details,
},

[MESSAGE.DEPRECATED_COMPONENT]: {
severity: LintMessageSeverity.Error,
ruleId: RULES["no-deprecated-component"],
Expand Down
153 changes: 152 additions & 1 deletion src/linter/ui5Types/SourceFileLinter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {MESSAGE} from "../messages.js";
import analyzeComponentJson from "./asyncComponentFlags.js";
import {deprecatedLibraries} from "../../utils/deprecations.js";
import {getPropertyName} from "./utils.js";
import {taskStart} from "../../utils/perf.js";

const log = getLogger("linter:ui5Types:SourceFileLinter");

Expand Down Expand Up @@ -45,6 +46,7 @@ export default class SourceFileLinter {
#reportCoverage: boolean;
#messageDetails: boolean;
#dataTypes: Record<string, string>;
#apiExtract: Record<string, Record<string, Record<string, string>>>;
#manifestContent: string | undefined;
#fileName: string;
#isComponent: boolean;
Expand All @@ -53,7 +55,8 @@ export default class SourceFileLinter {
context: LinterContext, resourcePath: ResourcePath,
sourceFile: ts.SourceFile, sourceMap: string | undefined, checker: ts.TypeChecker,
reportCoverage: boolean | undefined = false, messageDetails: boolean | undefined = false,
dataTypes: Record<string, string> | undefined, manifestContent?: string
dataTypes: Record<string, string> | undefined, manifestContent?: string,
apiExtract?: Record<string, Record<string, Record<string, string>>>
) {
this.#resourcePath = resourcePath;
this.#sourceFile = sourceFile;
Expand All @@ -67,6 +70,7 @@ export default class SourceFileLinter {
this.#fileName = path.basename(resourcePath);
this.#isComponent = this.#fileName === "Component.js" || this.#fileName === "Component.ts";
this.#dataTypes = dataTypes ?? {};
this.#apiExtract = apiExtract ?? {};
}

// eslint-disable-next-line @typescript-eslint/require-await
Expand Down Expand Up @@ -120,12 +124,159 @@ export default class SourceFileLinter {
context: this.#context,
checker: this.#checker,
});
} else if (ts.isPropertyDeclaration(node) && node.name.getText() === "metadata") {
const visitMetadataNodes = (childNode: ts.Node) => {
if (ts.isPropertyAssignment(childNode)) { // Skip nodes out of interest
this.analyzeMetadataProperty(childNode.name.getText(), childNode);
}

ts.forEachChild(childNode, visitMetadataNodes);
};

if (this.isUi5controlMetadataNode(node)) {
ts.forEachChild(node, visitMetadataNodes);
}
}

// Traverse the whole AST from top to bottom
ts.forEachChild(node, this.#boundVisitNode);
}

isUi5controlMetadataNode(node: ts.PropertyAssignment | ts.PropertyDeclaration): boolean {
// Go up the hierarchy chain to find whether the class extends from "sap/ui/base/ManagedObject"
const isObjectMetadataAncestor = (node: ts.ClassDeclaration): boolean => {
return node?.heritageClauses?.flatMap((parentClasses: ts.HeritageClause) => {
return parentClasses.types.flatMap((parentClass) => {
const parentClassType = this.#checker.getTypeAtLocation(parentClass);

return parentClassType.symbol?.declarations?.flatMap((declaration) => {
if (ts.isClassDeclaration(declaration)) {
if (declaration.name?.getText() === "ManagedObject" &&
ts.isModuleDeclaration(declaration.parent.parent) &&
declaration.parent.parent.name?.text === "sap/ui/base/ManagedObject") {
return true;
} else {
return isObjectMetadataAncestor(declaration);
}
} else {
return false;
}
});
});
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
}).reduce((acc, cur) => cur || acc, false) ?? false;
};

const metadataFound = taskStart("isPropertyInMetadata", this.#resourcePath, true);

if (node.name.getText() !== "metadata") {
metadataFound();
return false;
}

let parentNode: ts.Node = node.parent;
while (parentNode && parentNode.kind !== ts.SyntaxKind.ClassDeclaration) {
parentNode = parentNode.parent;
}

if (!parentNode) {
metadataFound();
return false;
d3xter666 marked this conversation as resolved.
Show resolved Hide resolved
}

const result = isObjectMetadataAncestor(parentNode as ts.ClassDeclaration);
metadataFound();
return result;
}

analyzeMetadataProperty(type: string, node: ts.PropertyAssignment) {
const analyzeMetadataDone = taskStart(`analyzeMetadataProperty: ${type}`, this.#resourcePath, true);
if (type === "interfaces") {
const deprecatedInterfaces = this.#apiExtract.deprecations.interfaces;

if (ts.isArrayLiteralExpression(node.initializer)) {
node.initializer.elements.forEach((elem) => {
const interfaceName = (elem as ts.StringLiteral).text;

if (deprecatedInterfaces[interfaceName]) {
this.#reporter.addMessage(MESSAGE.DEPRECATED_INTERFACE, {
interfaceName: interfaceName,
details: deprecatedInterfaces[interfaceName],
}, elem);
}
});
}
} else if (type === "altTypes" && ts.isArrayLiteralExpression(node.initializer)) {
const deprecatedTypes = {
...this.#apiExtract.deprecations.enums,
...this.#apiExtract.deprecations.typedefs,
};
node.initializer.elements.forEach((element) => {
const nodeType = ts.isStringLiteral(element) ? element.text : "";

if (deprecatedTypes[nodeType]) {
this.#reporter.addMessage(MESSAGE.DEPRECATED_TYPE, {
typeName: nodeType,
details: deprecatedTypes[nodeType],
}, element);
}
});
} else if (type === "defaultValue") {
const deprecatedTypes = {
...this.#apiExtract.deprecations.enums,
...this.#apiExtract.deprecations.typedefs,
};
const defaultValueType = ts.isStringLiteral(node.initializer) ?
node.initializer.text :
"";

const typeNode = node.parent.properties.find((prop) => {
return ts.isPropertyAssignment(prop) && prop.name.getText() === "type";
});

const fullyQuantifiedName = (typeNode &&
ts.isPropertyAssignment(typeNode) &&
ts.isStringLiteral(typeNode.initializer)) ?
[typeNode.initializer.text, defaultValueType].join(".") :
"";

if (deprecatedTypes[fullyQuantifiedName]) {
this.#reporter.addMessage(MESSAGE.DEPRECATED_TYPE, {
typeName: defaultValueType,
details: deprecatedTypes[fullyQuantifiedName],
}, node);
}
// This one is too generic and should always be at the last place
// It's for "types" and event arguments' types
} else if (ts.isStringLiteral(node.initializer)) {
const deprecatedTypes = {
...this.#apiExtract.deprecations.enums,
...this.#apiExtract.deprecations.typedefs,
} as Record<string, string>;

// Strip all the complex type definitions and create a list of "simple" types
// i.e. Record<string, Map<my.custom.type, Record<another.type, number[]>>>
// -> string, my.custom.type, another.type, number
const nodeTypes = node.initializer.text.replace(/\w+<|>|\[\]/gi, "")
.split(",").map((type) => type.trim());

nodeTypes.forEach((nodeType) => {
if (this.#apiExtract.deprecations.classes[nodeType]) {
this.#reporter.addMessage(MESSAGE.DEPRECATED_CLASS, {
className: nodeType,
details: this.#apiExtract.deprecations.classes[nodeType],
}, node.initializer);
} else if (deprecatedTypes[nodeType]) {
this.#reporter.addMessage(MESSAGE.DEPRECATED_TYPE, {
typeName: nodeType,
details: deprecatedTypes[nodeType],
}, node.initializer);
}
});
}
analyzeMetadataDone();
}

analyzeIdentifier(node: ts.Identifier) {
const type = this.#checker.getTypeAtLocation(node);
if (!type?.symbol || !this.isSymbolOfUi5OrThirdPartyType(type.symbol)) {
Expand Down
9 changes: 8 additions & 1 deletion src/linter/ui5Types/TypeLinter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,13 @@ export default class TypeChecker {
);
const dataTypes = JSON.parse(dataTypesFile) as Record<string, string>;

const apiExtractFile = await fs.readFile(
new URL("../../../resources/api-extract.json", import.meta.url),
{encoding: "utf-8"}
);
const apiExtract = JSON.parse(apiExtractFile) as
Record<string, Record<string, Record<string, string>>>;

const reportCoverage = this.#context.getReportCoverage();
const messageDetails = this.#context.getIncludeMessageDetails();
const typeCheckDone = taskStart("Linting all transpiled resources");
Expand All @@ -132,7 +139,7 @@ export default class TypeChecker {
this.#context, sourceFile.fileName,
sourceFile, sourceMap,
checker, reportCoverage, messageDetails, dataTypes,
manifestContent
manifestContent, apiExtract
);
await linter.lint();
linterDone();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
sap.ui.define(["sap/ui/core/Control", "sap/m/library", "mycustom/lib/FancyMultiPage"],
function (Control, library, FancyMultiPage) {
var DateTimeInputType = library.DateTimeInputType;
var FrameType = library.FrameType;
var FancyText = Control.extend("sap.fancy.Text", {
metadata: {
// sap.ui.commons.FormattedTextViewControl: deprecated
interfaces: ["sap.ui.commons.FormattedTextViewControl"],
library: "sap.fancy",
properties: {
text: {type: "string", group: "Data", defaultValue: "", bindable: "bindable"},
textShort: "sap.f.AvatarShape",
// sap.m.DateTimeInputType: deprecated, DateTimeInputType.DateTime: deprecated
textDirection: {type: "sap.m.DateTimeInputType", group: "Appearance", defaultValue: DateTimeInputType.DateTime},
// sap.m.DateTimeInputType: deprecated, "Date": deprecated
textDirectionB: {type: "sap.m.DateTimeInputType", group: "Appearance", defaultValue: "Date"},
// sap.m.FrameType: NOT deprecated, sap.m.FrameType.TwoThirds: deprecated
textAlign: {type: "sap.m.FrameType", group: "Appearance", defaultValue: FrameType.TwoThirds},
// sap.m.FrameType: NOT deprecated, "TwoThirds": deprecated
textAlignB: {type: "sap.m.FrameType", group: "Appearance", defaultValue: "TwoThirds"},
},
aggregations: {
// sap.f.Avatar: deprecated
myagg: {type: "sap.f.Avatar", multiple: false, visibility: "hiddenDeprecated"},
// sap.f.Avatar: deprecated
myaggShort: "sap.f.Avatar",
// sap.f.IllustratedMessageSize DataType: deprecated
tooltip: {type: "sap.ui.core.TooltipBase", altTypes: ["string", "sap.f.IllustratedMessageSize"], multiple: false},
beginColumnPages: {
type: "sap.ui.core.Control",
multiple: true,
forwarding: {
getter: "_getBeginColumn",
aggregation: "deprecatedPages",
},
},
},
associations: {
// sap.f.Avatar: deprecated
initialBeginColumnPage: {type: "sap.f.Avatar", multiple: false},
// sap.f.Avatar: deprecated
initialBeginColumnPageShort: "sap.f.Avatar",
},
events: {
eventA: {
parameters: {
layout: {
type: "sap.f.DynamicPageTitleArea", // deprecated
},
},
},
eventB: {
parameters: {
layout: {
type: "Promise<sap.f.DynamicPageTitleArea>", // deprecated
},
},
},
eventC: {
parameters: {
layout: "Promise<sap.f.AvatarShape>", // deprecated
newItems: {
type: "sap.m.P13nColumnsItem[]", // deprecated
},
},
},
},
},
});

return FancyText;
});
Loading