Skip to content

[DRAFT] Implementation of runtime checks for deprecated elements with v2 #3674

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

Draft
wants to merge 2 commits into
base: latest
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions src/pluginManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from "./api";
import { Logger } from "./logger";
import { Plugin } from "./plugin";
import { injectChangeDetection } from "./util/breakingChangeDetector";

const log = Logger.internal;

Expand Down Expand Up @@ -84,6 +85,8 @@ export class PluginManager {
constructor(api: HomebridgeAPI, options?: PluginManagerOptions) {
this.api = api;

injectChangeDetection();

if (options) {
if (options.customPluginPath) {
this.searchPaths.add(path.resolve(process.cwd(), options.customPluginPath));
Expand Down
147 changes: 147 additions & 0 deletions src/util/breakingChangeDetector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { existsSync, readFileSync } from "node:fs";
import { dirname, join, parse } from "node:path";
import { Characteristic, CharacteristicGetCallback, Service } from "hap-nodejs";
import { Logger } from "../logger";
import { PlatformAccessory } from "../platformAccessory";
const log = Logger.internal;

export function injectChangeDetection() {
setDeprecatedHapClasses();
setDeprecatedHapEnums();
setDeprecatedHapFunctions();
setDeprecatedHomebridgeFunctions();

log.info("Detection of breaking changes is enabled.");
}

function setDeprecatedHapClasses() {
let originalBatteryService = Service.BatteryService;

// Create a proxy to monitor the BatteryService class
Service.BatteryService = new Proxy(Service.BatteryService, {
construct(target, args) {
logDeprecationWarning("Service.BatteryService", "Service.Battery");
return new target(...args); // Proceed with the instantiation
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
get(target: any, prop: any, receiver) {
if (typeof target[prop] === "function") {
return (...args: unknown[]) => {
logDeprecationWarning("Service.BatteryService", "Service.Battery");
return target[prop].apply(this, args);
};
}
return Reflect.get(target, prop, receiver);
},
});

// Define a getter and setter for the static property
Object.defineProperty(Service, "BatteryService", {
get() {
logDeprecationWarning("Service.BatteryService", "Service.Battery");
return originalBatteryService;
},
set(newValue) {
logDeprecationWarning("Service.BatteryService", "Service.Battery");
originalBatteryService = newValue;
},
});
}

function setDeprecatedHapEnums() {
const FormatsProxy = new Proxy(Characteristic.Formats, {
get(target, prop) {
logDeprecationWarning("Characteristics.Formats", "api.hap.Formats");
return Reflect.get(target, prop);
},
});

const PermsProxy = new Proxy(Characteristic.Perms, {
get(target, prop) {
logDeprecationWarning("Characteristics.Perms", "api.hap.Perms");
return Reflect.get(target, prop);
},
});

const UnitsProxy = new Proxy(Characteristic.Units, {
get(target, prop) {
logDeprecationWarning("Characteristics.Units", "api.hap.Units");
return Reflect.get(target, prop);
},
});

// Replace the original enum with the proxy
Characteristic.Formats = FormatsProxy;
Characteristic.Perms = PermsProxy;
Characteristic.Units = UnitsProxy;
}

function setDeprecatedHapFunctions() {
const characteristicGetValue = Characteristic.prototype.getValue;

Characteristic.prototype.getValue = function (
this: Characteristic,
callback: CharacteristicGetCallback
) {
logDeprecationWarning("Characteristic.getValue()", "Characteristic.value");
return characteristicGetValue.call(this, callback);
};
}

function setDeprecatedHomebridgeFunctions() {
const platformAccessoryGetServiceByUUIDAndSubType =
PlatformAccessory.prototype.getServiceByUUIDAndSubType;

PlatformAccessory.prototype.getServiceByUUIDAndSubType = function (
this: unknown,
uuid: string,
subType: string
) {
logDeprecationWarning(
"platformAccessory.getServiceByUUIDAndSubType",
"platformAccessory.getService"
);
return platformAccessoryGetServiceByUUIDAndSubType.call(
this,
uuid,
subType
);
};
}

function logDeprecationWarning(deprecated: string, alternative?: string) {
const error = new Error();
const stackLines = error.stack?.split("\n");
const callerLine = stackLines ? stackLines[3] : "";

const filePathMatch = callerLine.match(/\((.*):\d+:\d+\)/);
const filePath = filePathMatch ? filePathMatch[1] : null;

if (filePath) {
const packageName = getPackageName(filePath);

const msg =
`Warning: The usage of '${deprecated}' is deprecated and will be removed with Homebridge 2.0.0.` +
(alternative
? ` Please upgrade the plugin to use '${alternative}' instead.`
: "");

log.warn(msg);
log.warn(`Affected plugin: ${packageName}`);
log.warn(`Accessed from: ${callerLine}`);
}
}

function getPackageName(filePath: string): string | null {
let currentDir = dirname(filePath);

while (currentDir !== parse(currentDir).root) {
const packageJsonPath = join(currentDir, "package.json");
if (existsSync(packageJsonPath)) {
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
return packageJson.name || null;
}
currentDir = dirname(currentDir);
}
return null;
}