Dark magic of code transformation

Valeriy Kuzmin, Lilt, 2021

Problem: DTO type sharing

#[derive(TypescriptDefinition, TypeScriptify, 
Deserialize, Serialize)]
#[serde(tag = "tag")]
pub enum InventoryAction {
    Unknown,
    Split { from: Uuid, count: i32 },
    Merge { from: Uuid, to: Uuid },
    Move { item: Uuid, index: i32 },
}

Rust

Problem: DTO type sharing

export type InventoryActionUnknown = { tag: "Unknown" };

export type InventoryActionSplit = {
    tag: "Split",
    from: string,
    count: number,
};

// more variants...

export type InventoryAction = InventoryActionUnknown | InventoryActionSplit;


export class InventoryActionBuilder {
    static InventoryActionUnknown = (): InventoryActionUnknown => ({
         tag: "Unknown"
    });
    static InventoryActionSplit = ({
         from,
         count
    }): InventoryActionSplit => ({
         tag: "Split",
         from,
         count
    });

    // more variants...
}

Typescript

Problem: DTO type sharing

export type InventoryAction =
  | { tag: 'Unknown' }
  | { tag: 'Split'; from: Uuid; count: number }
  | { tag: 'Merge'; from: Uuid; to: number }
  | { tag: 'Move'; item: Uuid; index: number };

typescript-definitions crate

Task 1: Advanced unions

export type InventoryAction = smth;

ExportNamedDeclaration with TSTypeAliasDeclaration

.replaceWith((ex: ASTPath<ExportNamedDeclaration>) => {
  if (ex.value.declaration.type !== 'TSTypeAliasDeclaration') {
  	return ex.value;
  }
  // ...
}

Task 1: Advanced unions

... type InventoryAction = Elem1 | Elem2;

type id = Identifier + type annotation of TSUnionType

// ...
// separate declaration from export
const p = ex.value.declaration;
// returns p.id.typeName if p.id.type === 'Identifier'
const mainUnionName = getTypeDeclaraiontName(p);
if (!mainUnionName) {
  return ex.value;
}
// extract the right part of the assignment
const union = p.typeAnnotation;
if (!isTSUnionType(union)) {
   // not a union, not interesting
  return ex.value;
}
// ...
        

Task 1: Advanced unions

type C = {tag: "A"} | {tag: "B"};

union.types ➟ [['CA','A'], ['CB', 'B']] ➟ Record<Tag, ExtractedTypeName>

union.types ➟ [['A', <A type obj>], ['B', <B type obj>]] ➟ Record<Tag, ExtractedType>

type CA = {tag: "A"};
type CB = {tag: "B"};
type C = CA | CB;

Task 1: Advanced unions

{tag: "A", foo: "bar"}
type CA = {tag: "A", foo: "bar"};

TSTypeLiteral

TSPropertySignature[]

key.type = 'Identifier'

valuer.type = 'Identifier' | 'TSNumberKeyWord' | 'TSStringKeyword' | 'TSLiteralType'

ObjectTypeAnnotation

Task 2: Builder classes

export type C = CA | CB;

ExportNamedDeclaration with type = 'TSTypeAliasDeclaration'

.find(j.ExportNamedDeclaration)
.insertAfter((ex: ASTPath<ExportNamedDeclaration>) => {
  if (ex.value.declaration.type !== 'TSTypeAliasDeclaration') {
    return [];
  }
  const union = ex.value.declaration.typeAnnotation;
// ...

Task 2: Builder classes

return j.exportNamedDeclaration(
  j.classDeclaration.from({
    id: j.identifier(builderClassName),
    body: j.classBody(
      union.types
      .map((subType) => {
        if (!isTsTypeReference(subType)) {
          return null;
        }
        if (!isIdentifier(subType.typeName)) {
          return null;
        }
        return j.classProperty.from({
          key: j.identifier(subType.typeName.name),
          static: true,
          accessibility: 'public',
          declare: false,
          value: makeBuilderMethod(
            subType.typeName.name,
            typeMap,
            j,
            mainTypeName
          ),
        });
      })
      .filter((t) => !!t)
    ),
  })
)

The end!

Questions?

jscodeshift-2-at-lilt

By Valeriy Kuzmin

jscodeshift-2-at-lilt

  • 162