Skip to content

Commit 5fc7567

Browse files
committed
fix: introduce deprecation decorator for the methods exposed in the public API
1 parent 9da5e48 commit 5fc7567

File tree

9 files changed

+215
-33
lines changed

9 files changed

+215
-33
lines changed

lib/commands/preview.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export class PreviewCommand implements ICommand {
3838

3939
public async canExecute(args: string[]): Promise<boolean> {
4040
if (args && args.length) {
41-
this.$errors.failWithHelp(`The arguments '${args.join(" ")}' are not valid for the preview command.`);
41+
this.$errors.failWithHelp(`The ${args.length > 1 ? "arguments" : "argument"} '${args.join(" ")}' ${args.length > 1 ? "are" : "is"} not valid for the preview command.`);
4242
}
4343

4444
if (!this.$options.force) {

lib/common/decorators.ts

+58-8
Original file line numberDiff line numberDiff line change
@@ -107,14 +107,14 @@ export function performanceLog(injector?: IInjector): any {
107107
performanceService.processExecutionData(trackName, start, end, args);
108108
} else {
109109
resolvedPromise
110-
.then(() => {
111-
end = performanceService.now();
112-
performanceService.processExecutionData(trackName, start, end, args);
113-
})
114-
.catch((err) => {
115-
end = performanceService.now();
116-
performanceService.processExecutionData(trackName, start, end, args);
117-
});
110+
.then(() => {
111+
end = performanceService.now();
112+
performanceService.processExecutionData(trackName, start, end, args);
113+
})
114+
.catch((err) => {
115+
end = performanceService.now();
116+
performanceService.processExecutionData(trackName, start, end, args);
117+
});
118118
}
119119

120120
return result;
@@ -130,3 +130,53 @@ export function performanceLog(injector?: IInjector): any {
130130
return descriptor;
131131
};
132132
}
133+
134+
// inspired by https://github.com/NativeScript/NativeScript/blob/55dfe25938569edbec89255008e5ad9804901305/tns-core-modules/globals/globals.ts#L121-L137
135+
export function deprecated(additionalInfo?: string, injector?: IInjector): any {
136+
const isDeprecatedMessage = " is deprecated.";
137+
return (target: Object, key: string, descriptor: TypedPropertyDescriptor<any>): TypedPropertyDescriptor<any> => {
138+
injector = injector || $injector;
139+
additionalInfo = additionalInfo || "";
140+
const $logger = <ILogger>injector.resolve("logger");
141+
if (descriptor) {
142+
if (descriptor.value) {
143+
// method
144+
const originalMethod = descriptor.value;
145+
146+
descriptor.value = function (...args: any[]) {
147+
$logger.warn(`${key.toString()}${isDeprecatedMessage} ${additionalInfo}`);
148+
149+
return originalMethod.apply(this, args);
150+
};
151+
152+
return descriptor;
153+
} else {
154+
// property
155+
if (descriptor.set) {
156+
const originalSetter = descriptor.set;
157+
descriptor.set = function (...args: any[]) {
158+
$logger.warn(`${key.toString()}${isDeprecatedMessage} ${additionalInfo}`);
159+
160+
originalSetter.apply(this, args);
161+
};
162+
}
163+
164+
if (descriptor.get) {
165+
const originalGetter = descriptor.get;
166+
descriptor.get = function (...args: any[]) {
167+
$logger.warn(`${key.toString()}${isDeprecatedMessage} ${additionalInfo}`);
168+
169+
return originalGetter.apply(this, args);
170+
};
171+
}
172+
173+
return descriptor;
174+
}
175+
} else {
176+
// class
177+
$logger.warn(`${((target && ((<any>target).name || ((<any>target).constructor && (<any>target).constructor.name))) || target)}${isDeprecatedMessage} ${additionalInfo}`);
178+
179+
return target;
180+
}
181+
};
182+
}

lib/common/definitions/logger.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ declare global {
1515

1616
interface ILogger {
1717
initialize(opts?: ILoggerOptions): void;
18-
initializeCliLogger(): void;
18+
initializeCliLogger(opts?: ILoggerOptions): void;
1919
getLevel(): string;
2020
fatal(formatStr?: any, ...args: any[]): void;
2121
error(formatStr?: any, ...args: any[]): void;

lib/common/errors.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as util from "util";
22
import * as path from "path";
33
import { SourceMapConsumer } from "source-map";
44
import { isInteractive } from "./helpers";
5+
import { deprecated } from "./decorators";
56

67
// we need this to overwrite .stack property (read-only in Error)
78
function Exception() {
@@ -128,7 +129,7 @@ export class Errors implements IErrors {
128129
return this.failWithOptions(opts, false, args);
129130
}
130131

131-
// DEPRECATED: use .fail instead
132+
@deprecated("Use `fail` instead.")
132133
public failWithoutHelp(optsOrFormatStr: string | IFailOptions, ...args: any[]): never {
133134
return this.fail(optsOrFormatStr, args);
134135
}

lib/common/logger/logger.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ export class Logger implements ILogger {
1212
private log4jsLogger: log4js.Logger = null;
1313
private passwordRegex = /(password=).*?(['&,]|$)|(password["']?\s*:\s*["']).*?(["'])/i;
1414
private passwordReplacement = "$1$3*******$2$4";
15+
private defaultLogLevel: LoggerLevel;
1516

16-
constructor(private $config: Config.IConfig,
17-
private $options: IOptions) {
17+
constructor(private $config: Config.IConfig) {
18+
this.defaultLogLevel = this.$config.DEBUG ? LoggerLevel.TRACE : LoggerLevel.INFO;
1819
}
1920

2021
@cache()
@@ -40,7 +41,7 @@ export class Logger implements ILogger {
4041
const categories: IDictionary<{ appenders: string[]; level: string; }> = {
4142
default: {
4243
appenders: ['out'],
43-
level: level || (this.$config.DEBUG ? "TRACE" : "INFO")
44+
level: level || this.defaultLogLevel
4445
}
4546
};
4647

@@ -49,12 +50,12 @@ export class Logger implements ILogger {
4950
this.log4jsLogger = log4js.getLogger();
5051
}
5152

52-
public initializeCliLogger(): void {
53+
public initializeCliLogger(opts?: ILoggerOptions): void {
5354
log4js.addLayout("cli", layout);
5455

5556
this.initialize({
5657
appenderOptions: { type: LoggerAppenders.cliAppender, layout: { type: "cli" } },
57-
level: <any>this.$options.log
58+
level: opts.level || this.defaultLogLevel
5859
});
5960
}
6061

lib/common/services/commands-service.ts

+8-6
Original file line numberDiff line numberDiff line change
@@ -91,16 +91,17 @@ export class CommandsService implements ICommandsService {
9191
return false;
9292
}
9393

94-
private printHelpSuggestion(commandName: string, commandArguments: string[]): Promise<void> {
95-
const commandHelp = `tns ${helpers.stringReplaceAll(this.beautifyCommandName(commandName), "|", " ")} --help`;
96-
this.$logger.printMarkdown(`Run \`${commandHelp}\` for more information.`);
94+
private printHelpSuggestion(commandName?: string): Promise<void> {
95+
const command = commandName ? helpers.stringReplaceAll(this.beautifyCommandName(commandName), "|", " ") + " " : "";
96+
const commandHelp = `tns ${command}--help`;
97+
this.$logger.printMarkdown(`\`Run '${commandHelp}' for more information.\``);
9798
return;
9899
}
99100

100101
private async executeCommandAction(commandName: string, commandArguments: string[], action: (_commandName: string, _commandArguments: string[]) => Promise<boolean>): Promise<boolean> {
101102
return this.$errors.beginCommand(
102103
() => action.apply(this, [commandName, commandArguments]),
103-
() => this.printHelpSuggestion(commandName, commandArguments));
104+
() => this.printHelpSuggestion(commandName));
104105
}
105106

106107
private async tryExecuteCommandAction(commandName: string, commandArguments: string[]): Promise<boolean> {
@@ -124,7 +125,7 @@ export class CommandsService implements ICommandsService {
124125
const command = this.$injector.resolveCommand(commandName);
125126
if (command) {
126127
this.$logger.error(`Command '${commandName} ${commandArguments}' cannot be executed.`);
127-
await this.printHelpSuggestion(commandName, commandArguments);
128+
await this.printHelpSuggestion(commandName);
128129
}
129130
}
130131
}
@@ -167,7 +168,8 @@ export class CommandsService implements ICommandsService {
167168
if (extensionData) {
168169
this.$logger.warn(extensionData.installationMessage);
169170
} else {
170-
this.$logger.fatal("Unknown command '%s'. Use '%s help' for help.", beautifiedName, this.$staticConfig.CLIENT_NAME.toLowerCase());
171+
this.$logger.error("Unknown command '%s'.", beautifiedName);
172+
await this.printHelpSuggestion();
171173
this.tryMatchCommand(commandName);
172174
}
173175

lib/common/test/unit-tests/decorators.ts

+133-9
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,14 @@ describe("decorators", () => {
5858
generatePublicApiFromExportedDecorator();
5959
const actualResult: any = $injector.publicApi.__modules__[moduleName][propertyName]();
6060
assert.deepEqual(actualResult, expectedResult);
61-
});
61+
});
6262

6363
it(`passes correct arguments to original function, when argument type is: ${_.isArray(expectedResult) ? "array" : typeof (expectedResult)}`, () => {
6464
$injector.register(moduleName, { propertyName: (arg: any) => arg });
6565
generatePublicApiFromExportedDecorator();
6666
const actualResult: any = $injector.publicApi.__modules__[moduleName][propertyName](expectedResult);
6767
assert.deepEqual(actualResult, expectedResult);
68-
});
68+
});
6969
});
7070

7171
it("returns Promise, which is resolved to correct value (function without arguments)", (done: mocha.Done) => {
@@ -202,7 +202,7 @@ describe("decorators", () => {
202202
generatePublicApiFromExportedDecorator();
203203
assert.throws(() => $injector.publicApi.__modules__[moduleName][propertyName](), errorMessage);
204204
});
205-
});
205+
});
206206

207207
describe("cache", () => {
208208
it("executes implementation of method only once and returns the same result each time whent it is called (number return type)", () => {
@@ -433,12 +433,12 @@ describe("decorators", () => {
433433
});
434434

435435
_.each(expectedResults, (expectedResult: any) => {
436-
it("returns proper result", () => {
436+
it("returns proper result", () => {
437437
const actualResult = testInstance.testMethod(expectedResult);
438438
assert.deepEqual(actualResult, expectedResult);
439439
});
440440

441-
it("returns proper result when async", () => {
441+
it("returns proper result when async", () => {
442442
const promise = testInstance.testAsyncMehtod(expectedResult);
443443

444444
assert.notDeepEqual(promise.then, undefined);
@@ -449,20 +449,20 @@ describe("decorators", () => {
449449
});
450450
});
451451

452-
it("method has same toString", () => {
452+
it("method has same toString", () => {
453453
assert.equal(testInstance.testMethod.toString(), undecoratedTestInstance.testMethod.toString());
454454
});
455455

456-
it("method has same name", () => {
456+
it("method has same name", () => {
457457
assert.equal(testInstance.testMethod.name, undecoratedTestInstance.testMethod.name);
458458
});
459459

460-
it("does not eat errors", () => {
460+
it("does not eat errors", () => {
461461
assert.throws(testInstance.throwMethod, testErrorMessage);
462462
assert.isRejected(testInstance.rejectMethod(), testErrorMessage);
463463
});
464464

465-
it("calls performance service on method call", async () => {
465+
it("calls performance service on method call", async () => {
466466
const performanceService = testInjector.resolve("performanceService");
467467
const processExecutionDataStub: sinon.SinonStub = sinon.stub(performanceService, "processExecutionData");
468468

@@ -486,4 +486,128 @@ describe("decorators", () => {
486486
checkSubCall(processExecutionDataStub.secondCall, "TestClass__testAsyncMehtod");
487487
});
488488
});
489+
490+
describe("deprecated", () => {
491+
const testDepMessage = "Just stop using this!";
492+
const warnings: string[] = [];
493+
let testInjector: IInjector;
494+
interface ITestInterface {
495+
testField: string;
496+
testProp: string;
497+
depMethodWithParam(arg: any): any;
498+
depMethodWithoutParam(): void;
499+
depAsyncMethod(arg: any): Promise<any>;
500+
nonDepMethod(): any;
501+
}
502+
let testInstance: ITestInterface;
503+
504+
function createTestInjector(): IInjector {
505+
testInjector = new Yok();
506+
testInjector.register("config", {});
507+
testInjector.register("options", {});
508+
testInjector.register("logger", {
509+
warn: (message: string) => {
510+
warnings.push(message);
511+
}
512+
});
513+
514+
return testInjector;
515+
}
516+
517+
beforeEach(() => {
518+
warnings.splice(0, warnings.length);
519+
testInjector = createTestInjector();
520+
521+
class TestClass implements ITestInterface {
522+
public testField: string = "test";
523+
524+
@decoratorsLib.deprecated(testDepMessage, testInjector)
525+
public get testProp(): string {
526+
return "hi";
527+
}
528+
529+
public set testProp(value: string) {
530+
return;
531+
}
532+
533+
@decoratorsLib.deprecated(testDepMessage, testInjector)
534+
depMethodWithParam(arg: any) {
535+
return arg;
536+
}
537+
538+
@decoratorsLib.deprecated(testDepMessage, testInjector)
539+
depMethodWithoutParam() {
540+
return;
541+
}
542+
543+
@decoratorsLib.deprecated(testDepMessage, testInjector)
544+
async depAsyncMethod(arg: any) {
545+
return Promise.resolve(arg);
546+
}
547+
548+
nonDepMethod() {
549+
return;
550+
}
551+
}
552+
553+
testInstance = new TestClass();
554+
});
555+
556+
it("method without params", () => {
557+
testInstance.depMethodWithoutParam();
558+
assert.equal(warnings.length, 1);
559+
assert.equal(warnings[0], `depMethodWithoutParam is deprecated. ${testDepMessage}`);
560+
});
561+
562+
it("method with params", () => {
563+
const param = 5;
564+
const result = testInstance.depMethodWithParam(param);
565+
assert.equal(result, param);
566+
assert.equal(warnings.length, 1);
567+
assert.equal(warnings[0], `depMethodWithParam is deprecated. ${testDepMessage}`);
568+
});
569+
570+
it("async method with params", async () => {
571+
const param = 5;
572+
const result = await testInstance.depAsyncMethod(param);
573+
assert.equal(result, param);
574+
assert.equal(warnings.length, 1);
575+
assert.equal(warnings[0], `depAsyncMethod is deprecated. ${testDepMessage}`);
576+
});
577+
578+
it("property getter", async () => {
579+
const result = testInstance.testProp;
580+
assert.equal(result, "hi");
581+
assert.equal(warnings.length, 1);
582+
assert.equal(warnings[0], `testProp is deprecated. ${testDepMessage}`);
583+
});
584+
585+
it("property setter", async () => {
586+
testInstance.testProp = "newValue";
587+
assert.equal(warnings.length, 1);
588+
assert.equal(warnings[0], `testProp is deprecated. ${testDepMessage}`);
589+
});
590+
591+
it("non deprecated field", async () => {
592+
const result = testInstance.testField;
593+
assert.equal(result, "test");
594+
assert.equal(warnings.length, 0);
595+
});
596+
597+
it("non deprecated method", () => {
598+
testInstance.nonDepMethod();
599+
assert.equal(warnings.length, 0);
600+
});
601+
602+
it("class", async () => {
603+
@decoratorsLib.deprecated(testDepMessage, testInjector)
604+
class TestClassDeprecated {
605+
}
606+
607+
const depClass = new TestClassDeprecated();
608+
assert.isNotNull(depClass);
609+
assert.equal(warnings.length, 1);
610+
assert.equal(warnings[0], `TestClassDeprecated is deprecated. ${testDepMessage}`);
611+
});
612+
});
489613
});

0 commit comments

Comments
 (0)