Skip to content

Commit a592904

Browse files
pmvaldthePunderWoman
authored andcommitted
fix(compiler-cli): allow custom/duplicate decorators for @Injectable classes in local compilation mode (#54139)
Custom/duplicate decorators break the deps tracker in local mode. But deps tracker only deals with non-injectable classes. So applying custom/duplicate decorators to `@Injectable` only classes does not disturb deps tracker and local compilation in general. There are also ~ 100 such cases in g3 which cannot be cleaned up. PR Close #54139
1 parent 4b1d948 commit a592904

4 files changed

Lines changed: 79 additions & 23 deletions

File tree

packages/compiler-cli/src/ngtsc/core/src/compiler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1181,7 +1181,7 @@ export class NgCompiler {
11811181
const traitCompiler = new TraitCompiler(
11821182
handlers, reflector, this.delegatingPerfRecorder, this.incrementalCompilation,
11831183
this.options.compileNonExportedClasses !== false, compilationMode, dtsTransforms,
1184-
semanticDepGraphUpdater, this.adapter);
1184+
semanticDepGraphUpdater, this.adapter, isCore);
11851185

11861186
// Template type-checking may use the `ProgramDriver` to produce new `ts.Program`(s). If this
11871187
// happens, they need to be tracked by the `NgCompiler`.

packages/compiler-cli/src/ngtsc/transform/src/compilation.ts

Lines changed: 60 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ export class TraitCompiler implements ProgramTypeCheckAdapter {
105105
private dtsTransforms: DtsTransformRegistry,
106106
private semanticDepGraphUpdater: SemanticDepGraphUpdater|null,
107107
private sourceFileTypeIdentifier: SourceFileTypeIdentifier,
108+
private readonly isCore: boolean,
108109
) {
109110
for (const handler of handlers) {
110111
this.handlersByName.set(handler.name, handler);
@@ -259,20 +260,20 @@ export class TraitCompiler implements ProgramTypeCheckAdapter {
259260
let record: ClassRecord|null = this.recordFor(clazz);
260261
let foundTraits: PendingTrait<unknown, unknown, SemanticSymbol|null, unknown>[] = [];
261262

262-
// A set to track the undetected decorators (= either non-Angular decorators or Angular
263-
// duplicate decorators) in local compilation mode. An error will be issued if such decorators
264-
// are found.
265-
const undetectedDecorators =
266-
this.compilationMode === CompilationMode.LOCAL ? new Set(decorators) : null;
263+
// A set to track the detected decorators in local compilation mode. An error will be issued if
264+
// undetected decorators (= either non-Angular decorators or Angular duplicate decorators) are
265+
// found.
266+
const detectedDecorators =
267+
this.compilationMode === CompilationMode.LOCAL ? new Set<Decorator>() : null;
267268

268269
for (const handler of this.handlers) {
269270
const result = handler.detect(clazz, decorators);
270271
if (result === undefined) {
271272
continue;
272273
}
273274

274-
if (undetectedDecorators !== null && result.decorator !== null) {
275-
undetectedDecorators.delete(result.decorator);
275+
if (detectedDecorators !== null && result.decorator !== null) {
276+
detectedDecorators.add(result.decorator);
276277
}
277278

278279
const isPrimaryHandler = handler.precedence === HandlerPrecedence.PRIMARY;
@@ -342,20 +343,30 @@ export class TraitCompiler implements ProgramTypeCheckAdapter {
342343
}
343344
}
344345

345-
if (undetectedDecorators !== null && undetectedDecorators.size > 0 && record !== null &&
346-
record.metaDiagnostics === null) {
347-
// Custom decorators found in local compilation mode! In this mode we don't support custom
348-
// decorators yet. But will eventually do (b/320536434). For now a temporary error is thrown.
349-
record.metaDiagnostics = [...undetectedDecorators].map(
350-
decorator => ({
351-
category: ts.DiagnosticCategory.Error,
352-
code: Number('-99' + ErrorCode.DECORATOR_UNEXPECTED),
353-
file: getSourceFile(clazz),
354-
start: decorator.node.getStart(),
355-
length: decorator.node.getWidth(),
356-
messageText:
357-
'In local compilation mode, Angular does not support custom decorators or duplicate Angular decorators. Ensure all class decorators are from Angular and each decorator is used at most once for each class.',
358-
}));
346+
// Local compilation uses the `DepsTracker` utility, which at the moment cannot track classes
347+
// mutated by custom/duplicate decorators. So we forbid custom/duplicate decorators on classes
348+
// used by deps tracker, i.e., Component, Directive, etc (basically everything except
349+
// Injectable)
350+
// TODO(b/320536434) Support custom/duplicate decorators for the DepsTracker utility.
351+
if (decorators !== null && detectedDecorators !== null &&
352+
detectedDecorators.size < decorators.length && record !== null &&
353+
record.metaDiagnostics === null &&
354+
hasDepsTrackerAffectingScopeDecorator(detectedDecorators, this.isCore)) {
355+
// Custom or duplicate decorators found in local compilation mode for a class which has
356+
// Angular decorators other than `@Injectable`! This is not supported yet. But will eventually
357+
// do (b/320536434). For now a temporary error is thrown.
358+
record.metaDiagnostics =
359+
decorators.filter(decorator => !detectedDecorators.has(decorator))
360+
.map(
361+
decorator => ({
362+
category: ts.DiagnosticCategory.Error,
363+
code: Number('-99' + ErrorCode.DECORATOR_UNEXPECTED),
364+
file: getSourceFile(clazz),
365+
start: decorator.node.getStart(),
366+
length: decorator.node.getWidth(),
367+
messageText:
368+
'In local compilation mode, Angular does not support custom decorators or duplicate Angular decorators (except for `@Injectable` classes). Ensure all class decorators are from Angular and each decorator is used at most once for each class.',
369+
}));
359370
record.traits = foundTraits = [];
360371
}
361372

@@ -715,3 +726,31 @@ function containsErrors(diagnostics: ts.Diagnostic[]|null): boolean {
715726
return diagnostics !== null &&
716727
diagnostics.some(diag => diag.category === ts.DiagnosticCategory.Error);
717728
}
729+
730+
/**
731+
* Duplicating the logic of `isAngularDecorator` in the package `ngtsc/annotations` which cannot be
732+
* imported here due to circular deps. This helper is needed temporary though until custom
733+
* decorators are supported by the runtime `DepsTracker`.
734+
*/
735+
function isInjectableDecorator(decorator: Decorator, isCore: boolean): boolean {
736+
if (isCore) {
737+
return decorator.name === 'Injectable';
738+
} else if (decorator.import !== null && decorator.import.from === '@angular/core') {
739+
return decorator.import.name === 'Injectable';
740+
}
741+
return false;
742+
}
743+
744+
/**
745+
* Scope decorators are those who participate in determining the scope of an NgModule or a
746+
* standalone component. They are: `@Component`, `@Directive`, `@Pipe` and `@NgModule`.
747+
*/
748+
function hasDepsTrackerAffectingScopeDecorator(decoratorSet: Set<Decorator>, isCore: boolean) {
749+
for (const decorator of decoratorSet) {
750+
if (!isInjectableDecorator(decorator, isCore)) {
751+
return true;
752+
}
753+
}
754+
755+
return false;
756+
}

packages/compiler-cli/src/ngtsc/transform/test/compilation_spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ runInEachFileSystem(() => {
4040
const reflectionHost = new TypeScriptReflectionHost(checker);
4141
const compiler = new TraitCompiler(
4242
handlers, reflectionHost, NOOP_PERF_RECORDER, NOOP_INCREMENTAL_BUILD, true,
43-
compilationMode, new DtsTransformRegistry(), null, fakeSfTypeIdentifier);
43+
compilationMode, new DtsTransformRegistry(), null, fakeSfTypeIdentifier,
44+
/** isCore */ false);
4445
const sourceFile = program.getSourceFile(filename)!;
4546

4647
return {compiler, sourceFile, program, filename: _('/' + filename)};

packages/compiler-cli/test/ngtsc/local_compilation_spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2040,6 +2040,22 @@ runInEachFileSystem(
20402040
expect(text1).toContain(
20412041
'In local compilation mode, Angular does not support custom decorators or duplicate Angular decorators');
20422042
});
2043+
2044+
it('should not produce diagnostic for duplicate Injectable decorator', () => {
2045+
env.write('test.ts', `
2046+
import {Injectable} from '@angular/core';
2047+
import {SomeServiceImpl} from './some-where';
2048+
2049+
@Injectable({providedIn: 'root', useExisting: SomeServiceImpl})
2050+
@Injectable({providedIn: 'root'})
2051+
export class SomeService {
2052+
}
2053+
`);
2054+
2055+
const errors = env.driveDiagnostics();
2056+
2057+
expect(errors.length).toBe(0);
2058+
});
20432059
});
20442060
});
20452061
});

0 commit comments

Comments
 (0)