KS-Killer

Sixian li

What is killswitch

A lightweight mechanism that can be used to roll back code change if a regression is found.

 

 

// KillSwitch.ts
export function isFixSomeBugKSActivated(): boolean {
  return _SPKillSwitch.isActivated(
    <KS-ID>,
    /* '07/27/2021', 'Fix some 🐞' */
  );
}

// App.ts
if (!isFixSomeBugKSActivated()) {
  newCode()
} else {
  oldStableCode()
}
Set-GridKillSwitch -KillSwitchId ${KS-Id} -State Active -Environment ProdBubble -Reason "Regression found" -HasCodeDefect $true -HowFound Manual 

In Merlin,

Motivation

The problem🤨

too many of them.

const commandBarStyle: string =
  this.props.isFullScreen && revert400PercentCheckKSActivated()
    ? styles.commandBarV2NoSticky
    : styles.commandBarV2;
...

  <Link
    data-interception='off'
    {...(fixLearnMoreFocusKSActivated() && { className: styles.infoLink })}
    {...(!fixLinkComponentHrefKSActivated()
      ? { href: 'https://google.com', target: '_blank' }
      : { role: 'link', onClick: this._openMoreInfoLink })}
  >
    {strings.learnMoreAboutAccess}
  </Link>
);
  • Code is hard to read
  • Bundle size is increased

NO TIME👋

LIVE demo

Idea

Stages

Find

Find the corresponding function declaration of a given KS ID

1.

Replace

Replace all the KS function calls with `false`

2.

Optimize

Remove dead code caused by the falsy return value

 

3.

e.g. `if (false) { ... }`

TypeScript Compiler API wrapper for static analysis and programmatic code changes.

  • Abstract Syntax Tree(AST)

 

Glossary

A tree representation of the abstract syntactic structure of source code

1. find

/**
 * Scan the project to find KS's declaration
 * @param project Target project
 * @param targetId KS ID
 * @param ksFilePath The file containing KS declaration. This boosts performance.
 */
const KS_IMPORT_SPECIFIER = '_SPKillSwitch';
const KS_ACTIVATED_METHOD = `${KS_IMPORT_SPECIFIER}.isActivated`;

export function findKSDeclaration(
  targetId: string,
): FunctionDeclaration[] {
  // May have multiple decls with the same id, so it's an array
  const result: FunctionDeclaration[] = [];

  let ksFiles: SourceFile[];
  // Declarations can only appear where we have _SPKillSwitch imports
  ksFiles = project.getSourceFiles().filter((f) =>
    f
      .getDescendantsOfKind(SyntaxKind.ImportSpecifier)
      .map((im) => im.getName())
      .includes(KS_IMPORT_SPECIFIER)
  );
}

ID ➡ KillSwitch function declaration

Assumption: KillSwitch has a function declaration, not inlined

export function findKSDeclaration(
  targetId: string,
): FunctionDeclaration[] {
  ...
  
  ksFiles.forEach((ksFile) => {
    const funDecls = ksFile.getChildrenOfKind(SyntaxKind.FunctionDeclaration);
    funDecls.forEach((funDecl) => {
      // restrict the structre
      // return _SPKillSwitch.isActivated(ID)
      const returnStatement = funDecl.getFirstDescendantByKind(SyntaxKind.ReturnStatement);
      const callExp = returnStatement?.getExpressionIfKind(SyntaxKind.CallExpression);
      const accessExp = callExp?.getExpressionIfKind(SyntaxKind.PropertyAccessExpression);
      // wrong structure or ID doesn't match, skip
      if (
        accessExp?.getText() != KS_ACTIVATED_METHOD ||
        callExp?.getArguments()[0]?.getText() !== `'${targetId}'`
      ) {
        return;
      }
      result.push(funDecl);
    });
  });

  return result;
}

ID ➡ KillSwitch function declaration

Assumption: KillSwitch has a function declaration, not inlined

export function isFixSomeBugKSActivated(): boolean {
  return _SPKillSwitch.isActivated(
    <KS-ID>,
    /* '07/27/2021', 'Fix some 🐞' */
  );
}

2. Replace

if (!isFixMyBrokenLegKSActivated()) {
  doAKickflip();
} else {
  cry();
}

if (!false) {
  doAKickflip()
} else {
  cry();
}

if (true) {
  doAKickflip()
} else {
  cry();
}

// after optimization
doAKickflip() // don't cry

Example

/**
 * Replace KS calls with 'false'
 * @param ksDecl KS Declarations. Used to find references.
 * @returns A list of nodes to be optimized.
 */
export function replaceFunCallWithFalse(ksDecl: FunctionDeclaration): {
  // nodes affected by this replacement, to be optimized
  workList: Set<Node<ts.Node>>; 
  // files that the reference is in, used for recursive optimization
  refFiles: Array<SourceFile>;  
} {
  
}
export function replaceFunCallWithFalse(ksDecl: FunctionDeclaration): {
  workList: Set<Node<ts.Node>>; 
  refFiles: Array<SourceFile>;  
} {
  const workList = new Set<Node<ts.Node>>();
  const refFiles = new Set<SourceFile>();
  
  console.log('Finding references...');
  const refSymbols = ksDecl.findReferences(); // from ts-morph
  refSymbols.forEach((refSymbol) => {
    refSymbol.getReferences().forEach((ref) => {
      const refNode = ref.getNode();
      const parent = refNode.getParent();
      // not a function call(e.g. declaration), skip
      if (!(parent.getKind() === SyntaxKind.CallExpression)) {
        return;
      }
      refFiles.add(refNode.getSourceFile());
      // if it's negated, replace the whole thing with true
      const negation = parent.getParentIfKind(SyntaxKind.PrefixUnaryExpression);
      if (negation?.getOperatorToken() === SyntaxKind.ExclamationToken) {
        workList.add(negation.replaceWithText('true').getParent());
      } else {
        workList.add(parent.replaceWithText('false').getParent());
      }
    });
  });
  return { workList, refFiles: [...refFiles] };
}

3. optimize

simplification

KS usage patterns in the 5 most frequently developed projects in our team, 712 KS references in total

sp-mytopics-webpart, sp-topic-viewer-webpart, topic-webparts, sp-pages, sp-topic-shared, sp-component-utilities

Example

YES, I'm the person who writes tests for a Hackathon project.

These examples are taken from tests I wrote.

smile

Example - binry expression

describe('Binary Expression', () => {
  describe('Simple expression', () => {
    it('should handle && correctly', () => {
      const { sourceFile } = getInfoFromText(`
      true && A(), A() && true, false && A(), A() && false
    `);
      handleBinaryExps(sourceFile);
      expect(sourceFile.getText().trim()).toBe(`A(), A(), false, false`);
    });

    it('should handle || correctly', () => {
     `true || A(), A() || true, false || A(), A() || false`
     `true, true, A(), A()`
    });

    it('should handle undefined/null', () => {
      `undefined && A(), undefined || A(), null && A(), null || A()`
      `undefined, A(), null, A()`
    });
  });

  describe('Nested expression', () => {
    it('should handle parent exp', () => {
      `(true || A()) || B()`
      `true`
    });
  });
});
describe('Conditional Expression', () => {
  describe('Simple conditional', () => {
    it('should remove else branch when cond is always true', () => {
      `true ? A() : B`
      `A()`
    });
    it('should remove then branch when cond is always false', () => {
      `false ? A() : B`
      `B`
    });
  });
})

describe('Nested conditional', () => {
  it('should handle nested conditionals', () => {
    `(true ? A() : B()) ? C() : (false ? D() : E())`
    `A() ? C() : E()`
  });
});

Example - conditional expression

Inside ➡ outside

export type HandlerReturnType = Node<ts.Node> | undefined;

export function optimizeNode(node: Node<ts.Node>) {
  if (!node || node.wasForgotten()) return;
  let newWork: HandlerReturnType;
  switch (node.getKind()) {
    case SyntaxKind.BinaryExpression:
      newWork = handleBinaryExp(node as BinaryExpression);
      break;
    case SyntaxKind.ConditionalExpression:
      newWork = handleConditionalExp(node as ConditionalExpression);
      break;
    case SyntaxKind.IfStatement:
      handleIf(node as IfStatement);
      break;
  }
  // if there's any optimization happened
  if (newWork) {
    optimizeNode(newWork.getParent());
  }
}

For simplicity. Should also handle variable assignments like

`const a = isKSActivated()`
export function handleConditionalExp(exp: ConditionalExpression): HandlerReturnType {
  const cond = exp.getCondition();
  let newWork: HandlerReturnType;
  if (cond.getKind() === SyntaxKind.TrueKeyword) {
    newWork = exp.replaceWithText(exp.getWhenTrue().getText());
  } else if (cond.getKind() === SyntaxKind.FalseKeyword) {
    newWork = exp.replaceWithText(exp.getWhenFalse().getText());
  }
  return tryUnwrapParenthese(newWork);
}
export function handleConditionalExp(exp: ConditionalExpression): HandlerReturnType {
  const cond = exp.getCondition();
  let newWork: HandlerReturnType;
  if (cond.getKind() === SyntaxKind.TrueKeyword) {
    newWork = exp.replaceWithText(exp.getWhenTrue().getText());
  } else if (cond.getKind() === SyntaxKind.FalseKeyword) {
    newWork = exp.replaceWithText(exp.getWhenFalse().getText());
  }
  return tryUnwrapParenthese(newWork);
}
export function handleIf(ifStmt: IfStatement): void {
  try {
    // check if our replacement introduces const conditions
    const cond = eval(ifStmt.getExpression().getText());

    // always true
    if (cond) {
      const thenBlock = ifStmt.getThenStatement();
      ifStmt.replaceWithText(unwrapBlock(thenBlock));
    } else {
      // always false
      const elseBlock = ifStmt.getElseStatement();
      if (!elseBlock) {
        ifStmt.remove();
      } else {
        ifStmt.replaceWithText(unwrapBlock(elseBlock));
      }
    }
  } catch (err) {
    // unable to optimize, skip
  }
}

Finally...

 

export function run(id: string, projectPath: string, ksFilePath?: string) {
  const project = new Project();
  project.addSourceFilesAtPaths(path.join(projectPath, `src/**/*.{ts,tsx}`));
  const ksDecls = findKSDeclaration(project, id, ksFilePath);
}

Finally...

 

export function run(id: string, projectPath: string, ksFilePath?: string) {
  const project = new Project();
  project.addSourceFilesAtPaths(path.join(projectPath, `src/**/*.{ts,tsx}`));
  const ksDecls = findKSDeclaration(project, id, ksFilePath);

  ksDecls.map(replaceFunCallWithFalse).forEach(({ workList, refFiles }) => {
    optimize(workList);

    // run while new changes happened
    let changed: boolean;
    do {
      changed = false;
      refFiles.forEach((f) => {
        const lastWidth = f.getFullWidth();
        f.fixUnusedIdentifiers();
        if (f.getFullWidth() !== lastWidth) {
          changed = true;
        }
      });
    } while (changed);
  });
}

Finally...

 

export function run(id: string, projectPath: string, ksFilePath?: string) {
  const project = new Project();
  project.addSourceFilesAtPaths(path.join(projectPath, `src/**/*.{ts,tsx}`));
  const ksDecls = findKSDeclaration(project, id, ksFilePath);

  ksDecls.map(replaceFunCallWithFalse).forEach(({ workList, refFiles }) => {
    optimize(workList);

    // run while new changes happened
    let changed: boolean;
    do {
      changed = false;
      refFiles.forEach((f) => {
        const lastWidth = f.getFullWidth();
        f.fixUnusedIdentifiers();
        if (f.getFullWidth() !== lastWidth) {
          changed = true;
        }
      });
    } while (changed);
  });
  
  // remove KS function declarations
  ksDecls.forEach((decl) => decl.remove());
  project.save();
}

FUture

  • A function with a clear name
  • A dedicated KS file
  •  `_SPKillSwitch`
  • VSCode snippet for KS
  • Inline
  • Scattered, everywhere
  • Deprecated KS library
  • Copy, paste

Encourage best practices

or just hides the related KillSwitch blocks to improve readability

Integrated into pipeline, automatically graduate the KS, create a PR, notify the owner to review

more automatic

MOre productive

A VSCode extension that graduates the selected KillSwitch

Test and iterate. 

more reliable

open source

Thank you

— by Sara Levin, CEO