Skip to content

Commit

Permalink
fix(compiler-cli): input transform in local compilation mode (angular…
Browse files Browse the repository at this point in the history
…#53645)

Currently compiling input transform in local mode breaks, since compiler does static analysis for the transform function, and this cannot be done in local mode if the function is imported from another compilation unit. In this fix the static analysis is ditched in local mode.

PR Close angular#53645
  • Loading branch information
pmvald authored and amilamen committed Jan 26, 2024
1 parent 745bfa2 commit b77b950
Show file tree
Hide file tree
Showing 2 changed files with 139 additions and 12 deletions.
47 changes: 35 additions & 12 deletions packages/compiler-cli/src/ngtsc/annotations/directive/src/shared.ts
Expand Up @@ -78,9 +78,10 @@ export function extractDirectiveMetadata(

// Construct the map of inputs both from the @Directive/@Component
// decorator, and the decorated fields.
const inputsFromMeta = parseInputsArray(clazz, directive, evaluator, reflector, refEmitter);
const inputsFromFields =
parseInputFields(clazz, members, evaluator, reflector, refEmitter, coreModule);
const inputsFromMeta =
parseInputsArray(clazz, directive, evaluator, reflector, refEmitter, compilationMode);
const inputsFromFields = parseInputFields(
clazz, members, evaluator, reflector, refEmitter, coreModule, compilationMode);
const inputs = ClassPropertyMapping.fromMappedObject({...inputsFromMeta, ...inputsFromFields});

// And outputs.
Expand Down Expand Up @@ -616,8 +617,8 @@ function parseDecoratedFields(
/** Parses the `inputs` array of a directive/component decorator. */
function parseInputsArray(
clazz: ClassDeclaration, decoratorMetadata: Map<string, ts.Expression>,
evaluator: PartialEvaluator, reflector: ReflectionHost,
refEmitter: ReferenceEmitter): Record<string, InputMapping> {
evaluator: PartialEvaluator, reflector: ReflectionHost, refEmitter: ReferenceEmitter,
compilationMode: CompilationMode): Record<string, InputMapping> {
const inputsField = decoratorMetadata.get('inputs');

if (inputsField === undefined) {
Expand Down Expand Up @@ -669,7 +670,7 @@ function parseInputsArray(
}

transform = parseDecoratorInputTransformFunction(
clazz, name, transformValue, reflector, refEmitter);
clazz, name, transformValue, reflector, refEmitter, compilationMode);
}

inputs[name] = {
Expand Down Expand Up @@ -711,8 +712,8 @@ function tryGetDecoratorOnMember(

function tryParseInputFieldMapping(
clazz: ClassDeclaration, member: ClassMember, evaluator: PartialEvaluator,
reflector: ReflectionHost, coreModule: string|undefined,
refEmitter: ReferenceEmitter): InputMapping|null {
reflector: ReflectionHost, coreModule: string|undefined, refEmitter: ReferenceEmitter,
compilationMode: CompilationMode): InputMapping|null {
const classPropertyName = member.name;

// Look for a decorator first.
Expand Down Expand Up @@ -757,7 +758,7 @@ function tryParseInputFieldMapping(
}

transform = parseDecoratorInputTransformFunction(
clazz, classPropertyName, transformValue, reflector, refEmitter);
clazz, classPropertyName, transformValue, reflector, refEmitter, compilationMode);
}

return {
Expand Down Expand Up @@ -797,8 +798,8 @@ function tryParseInputFieldMapping(
/** Parses the class members that declare inputs (via decorator or initializer). */
function parseInputFields(
clazz: ClassDeclaration, members: ClassMember[], evaluator: PartialEvaluator,
reflector: ReflectionHost, refEmitter: ReferenceEmitter,
coreModule: string|undefined): Record<string, InputMapping> {
reflector: ReflectionHost, refEmitter: ReferenceEmitter, coreModule: string|undefined,
compilationMode: CompilationMode): Record<string, InputMapping> {
const inputs = {} as Record<string, InputMapping>;

for (const member of members) {
Expand All @@ -814,6 +815,7 @@ function parseInputFields(
reflector,
coreModule,
refEmitter,
compilationMode,
);
if (inputMapping !== null) {
inputs[classPropertyName] = inputMapping;
Expand All @@ -836,7 +838,28 @@ function parseInputFields(
*/
export function parseDecoratorInputTransformFunction(
clazz: ClassDeclaration, classPropertyName: string, value: DynamicValue|Reference,
reflector: ReflectionHost, refEmitter: ReferenceEmitter): DecoratorInputTransform {
reflector: ReflectionHost, refEmitter: ReferenceEmitter,
compilationMode: CompilationMode): DecoratorInputTransform {
// In local compilation mode we can skip type checking the function args. This is because usually
// the type check is done in a separate build which runs in full compilation mode. So here we skip
// all the diagnostics.
if (compilationMode === CompilationMode.LOCAL) {
const node =
value instanceof Reference ? value.getIdentityIn(clazz.getSourceFile()) : value.node;

// This should never be null since we know the reference originates
// from the same file, but we null check it just in case.
if (node === null) {
throw createValueHasWrongTypeError(
value.node, value, 'Input transform function could not be referenced');
}

return {
node,
type: new Reference(ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword))
};
}

const definition = reflector.getDefinitionOfFunction(value.node);

if (definition === null) {
Expand Down
104 changes: 104 additions & 0 deletions packages/compiler-cli/test/ngtsc/local_compilation_spec.ts
Expand Up @@ -1033,5 +1033,109 @@ runInEachFileSystem(() => {
.toContain('ɵɵsetNgModuleScope(AppModule, { declarations: [App] }); })();');
});
});

describe('input transform', () => {
it('should generate input info for transform function imported externally using name', () => {
env.write('test.ts', `
import {Component, NgModule, Input} from '@angular/core';
import {externalFunc} from './some_where';
@Component({
template: '<span>{{x}}</span>',
})
export class Main {
@Input({transform: externalFunc})
x: string = '';
}
`);

env.driveMain();
const jsContents = env.getContents('test.js');

expect(jsContents).toContain('inputs: { x: ["x", "x", externalFunc] }');
});

it('should generate input info for transform function imported externally using namespace',
() => {
env.write('test.ts', `
import {Component, NgModule, Input} from '@angular/core';
import * as n from './some_where';
@Component({
template: '<span>{{x}}</span>',
})
export class Main {
@Input({transform: n.externalFunc})
x: string = '';
}
`);

env.driveMain();
const jsContents = env.getContents('test.js');

expect(jsContents).toContain('inputs: { x: ["x", "x", n.externalFunc] }');
});

it('should generate input info for transform function defined locally', () => {
env.write('test.ts', `
import {Component, NgModule, Input} from '@angular/core';
@Component({
template: '<span>{{x}}</span>',
})
export class Main {
@Input({transform: localFunc})
x: string = '';
}
function localFunc(value: string) {
return value;
}
`);

env.driveMain();
const jsContents = env.getContents('test.js');

expect(jsContents).toContain('inputs: { x: ["x", "x", localFunc] }');
});

it('should generate input info for inline transform function', () => {
env.write('test.ts', `
import {Component, NgModule, Input} from '@angular/core';
@Component({
template: '<span>{{x}}</span>',
})
export class Main {
@Input({transform: (v: string) => v + 'TRANSFORMED!'})
x: string = '';
}
`);

env.driveMain();
const jsContents = env.getContents('test.js');

expect(jsContents).toContain('inputs: { x: ["x", "x", (v) => v + \'TRANSFORMED!\'] }');
});

it('should not check inline function param type', () => {
env.write('test.ts', `
import {Component, NgModule, Input} from '@angular/core';
@Component({
template: '<span>{{x}}</span>',
})
export class Main {
@Input({transform: v => v + 'TRANSFORMED!'})
x: string = '';
}
`);

env.driveMain();
const jsContents = env.getContents('test.js');

expect(jsContents).toContain('inputs: { x: ["x", "x", v => v + \'TRANSFORMED!\'] }');
});
});
});
});

0 comments on commit b77b950

Please sign in to comment.