AST selector for eslint rule for Angular ngOnChanges method
04:57 11 Nov 2023

The idea is to validate ngOnChanges Angular method.

  • It should have changes of type SimpleChanges as an argument
  • Inside the function when accessing the property currentChanges should be optionally chained

So an example of invalid code is:

public ngOnChanges(notChangesName: NotSimpleShanges) {
  notChangesName.property.currentValue;
}

And an example of valid code is:

public ngOnChanges(changes: SimpleShanges) {
  changes.property?.currentValue;
}

The best I came with is

"no-restricted-syntax": [
  "error",
  {
    "message": "Changes currentValue should be optionally chained",
    "selector": "MemberExpression[optional=false] > Identifier[name=\"currentValue\"]"
  },
  {
    "message": "ngOnChanges should have 'changes' as an argument of type 'SimpleChanges'",
    "selector": "MethodDefinition[key.name=\"ngOnChanges\"] > FunctionExpression > Identifier[name!=\"changes\"]"
  },
  {
    "message": "ngOnChanges should have 'changes' as an argument of type 'SimpleChanges'",
    "selector": "MethodDefinition[key.name=\"ngOnChanges\"] > FunctionExpression > BlockStatement:not(:has(Parameter))"
  }
]

The problems are:

  1. Selector MemberExpression[optional=false] > Identifier[name=\"currentValue\"] checks currentValue is optionally chained to any variable. I need it to be checked if the parent has name changes (changes.field.currentValue)

  2. Type of changes is not checked by selector MethodDefinition[key.name=\"ngOnChanges\"] > FunctionExpression > Identifier[name!=\"changes\"]. It should be of type SimpleChanges

UPD: Current solution is:

"no-restricted-syntax": [
  "error",
  {
    "message": "Properties 'currentValue | firstChange | previousValue | isFirstChange' should be optional",
    "selector": "MethodDefinition[key.name='ngOnChanges'] MemberExpression[object.object.name='changes'][optional=false][property.name=/.*/]"
  },
  {
    "message": "ngOnChanges should have 'changes' as an argument",
    "selector": "MethodDefinition[key.name='ngOnChanges'] > FunctionExpression > Identifier[name!='changes']"
  },
  {
    "message": "ngOnChanges should have 'changes: NgChanges' as an argument",
    "selector": "MethodDefinition[key.name='ngOnChanges'] > FunctionExpression > Identifier >  TSTypeAnnotation > TSTypeReference > Identifier[name!='SimpleChanges']"
  },
  {
    "message": "ngOnChanges should have one argument 'changes: NgChanges'",
    "selector": "MethodDefinition[key.name='ngOnChanges'] > FunctionExpression[params.length!=1]"
  }
]
angular typescript eslint abstract-syntax-tree