Skip to content

useFactory with block-body functions silently dropped in providers #203

@ashley-hunter

Description

@ashley-hunter

When any decorator property contains an expression tree that includes a block-body arrow function (() => { ... }) or a function() expression, the function content (except the return) is silently dropped from the compiled output. The OXC-to-output-AST converter returns None when it encounters statement types it can't represent (e.g., if/else, for, try/catch, variable declarations).

Reproduction

  import { Component, inject } from '@angular/core';

  const MY_TOKEN = 'MY_TOKEN';

  @Component({
    selector: 'my-component',
    template: '<div>hello</div>',
    providers: [                                                                                                                
      {
        provide: MY_TOKEN,                                                                                                      
        useFactory: () => {                                       
          const config = inject(AppConfig);
          if (config.useMock) {
            return new MockService();
          }
          return new RealService(config);                                                                                       
        }
      }                                                                                                                         
    ]                                                             
  })
  export class MyComponent {}

Current output (broken)

The const declaration and if block are silently stripped. Only the final return survives:

  import { Component, inject } from '@angular/core';
  import * as i0 from '@angular/core';

  const MY_TOKEN = 'MY_TOKEN';

  export class MyComponent {
    static ɵfac = function MyComponent_Factory(__ngFactoryType__) {
      return new (__ngFactoryType__ || MyComponent)();
    };
    static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({type:MyComponent,selectors:[["my-component"]],
        features:[i0.ɵɵProvidersFeature([{provide:MY_TOKEN,useFactory:() =>{
          return new RealService(config);  // ⚠️  config is undefined — was declared in dropped const
        }}])],decls:2,vars:0,template:function MyComponent_Template(rf,ctx) {
          if ((rf & 1)) {
            i0.ɵɵelementStart(0,"div");
            i0.ɵɵtext(1,"hello");
            i0.ɵɵelementEnd();
          }
        },encapsulation:2});
  }

The const config = inject(AppConfig) and entire if (config.useMock) { ... } block are silently dropped. The remaining return new RealService(config) references config which no longer exists, causing an error at runtime.

For comparison, the expression-body version emits correctly:

  useFactory: () => new RealService(inject(AppConfig))                                                                          

Root cause

convert_oxc_expression recurses through the expression tree. When it hits an arrow function with a block body, it can only handle return and expression statements. Any other statement type (const, if, for, try, etc.) causes the converter to return None.

Suggested fix

Thread source_text through to all convert_oxc_expression call sites in decorator extraction, so block-body functions can be preserved as raw source. The compiler doesn't need to analyze user code in these positions - it just needs to emit it verbatim.

Alternative: extend OutputStatement to model all JavaScript statement types

Instead of preserving factory bodies as raw source text, the other approach would be to add the missing statement variants to the output AST's OutputStatement enum. Currently it only models DeclareVar, DeclareFunction, Expression, Return, and If. The missing types that cause statements to be silently dropped include:

  • const/let declarations with destructuring patterns
  • for, for...in, for...of loops
  • while, do...while loops
  • try/catch/finally
  • switch/case
  • throw
  • labeled statements, break, continue

Each new variant would also need to be handled across every phase that walks the output AST — the emitter, slot allocation, chaining, var counting, variable optimization, advance generation, reification, expression transformation, cloning, equivalence checks, and the recursive visitor. This is a significant amount of work for statement types the compiler never needs to analyze or transform — in these positions, the user's code is just being passed through verbatim to the output.

This approach would be the right choice if the compiler needed to inspect or transform the contents of factory bodies (e.g., rewriting identifiers, tree-shaking unused branches). But since these are opaque user expressions that just need to survive compilation intact, the raw source approach is simpler and handles any valid JavaScript without needing to model it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions