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.
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
ks-killer
By Sixian Li
ks-killer
- 317