Initial commit

This commit is contained in:
2025-03-07 19:22:02 +01:00
commit 4a98255d83
55743 changed files with 5280367 additions and 0 deletions
@@ -0,0 +1,136 @@
import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
import { Reference } from "@typescript-eslint/scope-manager";
import { isLoadCall, parsePropertiesArgument } from "../utils/load";
import {
findPropertiesRead,
findOfficeApiReferences,
OfficeApiReference,
findCallExpression,
} from "../utils/utils";
export default ESLintUtils.RuleCreator(
() =>
"https://docs.microsoft.com/office/dev/add-ins/develop/application-specific-api-model#load",
)({
name: "call-sync-after-load",
meta: {
type: "suggestion",
messages: {
callSyncAfterLoad:
"Call context.sync() after calling load on '{{name}}' for the property '{{loadValue}}' and before reading the property.",
},
docs: {
description:
"Always call context.sync() between loading one or more properties on objects and reading any of those properties.",
},
schema: [],
},
create: function (context) {
const sourceCode = context.sourceCode ?? context.getSourceCode();
type VariableProperty = {
variable: string;
property: string;
};
class VariablePropertySet extends Set {
add(variableProperty: VariableProperty) {
return super.add(JSON.stringify(variableProperty));
}
has(variableProperty: VariableProperty) {
return super.has(JSON.stringify(variableProperty));
}
}
let apiReferences: OfficeApiReference[] = [];
function findLoadBeforeSync(): void {
const needSync: VariablePropertySet = new VariablePropertySet();
apiReferences.forEach((apiReference) => {
const operation = apiReference.operation;
const reference: Reference = apiReference.reference;
const identifier: TSESTree.Node = reference.identifier;
const variable = reference.resolved;
if (operation === "Load" && variable) {
const propertiesArgument = getPropertiesArgument(identifier);
const propertyNames: string[] = propertiesArgument
? parsePropertiesArgument(propertiesArgument)
: ["*"];
propertyNames.forEach((propertyName: string) => {
needSync.add({
variable: variable.name,
property: propertyName,
});
});
} else if (operation === "Sync") {
needSync.clear();
} else if (operation === "Read" && variable) {
const propertyName: string = findPropertiesRead(
reference.identifier.parent,
);
if (
needSync.has({ variable: variable.name, property: propertyName }) ||
needSync.has({ variable: variable.name, property: "*" })
) {
const node = reference.identifier;
context.report({
node: node,
messageId: "callSyncAfterLoad",
data: { name: node.name, loadValue: propertyName },
});
}
}
});
}
function getPropertiesArgument(
identifier: TSESTree.Identifier | TSESTree.JSXIdentifier,
): TSESTree.CallExpressionArgument | undefined {
let propertiesArgument;
if (
identifier.parent?.type === TSESTree.AST_NODE_TYPES.MemberExpression
) {
// Look for <obj>.load(...) call
const methodCall = findCallExpression(identifier.parent);
if (methodCall && isLoadCall(methodCall)) {
propertiesArgument = methodCall.arguments[0];
}
} else if (
identifier.parent?.type === TSESTree.AST_NODE_TYPES.CallExpression
) {
// Look for context.load(<obj>, "...") call
const args: TSESTree.CallExpressionArgument[] =
identifier.parent.arguments;
if (
isLoadCall(identifier.parent) &&
args[0] == identifier &&
args.length < 3
) {
propertiesArgument = args[1];
}
}
return propertiesArgument;
}
return {
Program(node) {
const scope = sourceCode.getScope
? sourceCode.getScope(node)
: context.getScope();
apiReferences = findOfficeApiReferences(scope);
apiReferences.sort((left, right) => {
return (
left.reference.identifier.range[1] -
right.reference.identifier.range[1]
);
});
findLoadBeforeSync();
},
};
},
defaultOptions: [],
});
@@ -0,0 +1,86 @@
import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
import { Variable } from "@typescript-eslint/scope-manager";
import { findTopMemberExpression } from "../utils/utils";
import { findOfficeApiReferences, OfficeApiReference } from "../utils/utils";
export default ESLintUtils.RuleCreator(
() =>
"https://docs.microsoft.com/office/dev/add-ins/develop/application-specific-api-model#sync",
)({
name: "call-sync-before-read",
meta: {
type: "problem",
messages: {
callSync: "Call context.sync() before trying to read '{{name}}'.",
},
docs: {
description:
"Always call load on the object's properties followed by a context.sync() before reading them.",
},
schema: [],
},
create: function (context) {
const sourceCode = context.sourceCode ?? context.getSourceCode();
let apiReferences: OfficeApiReference[] = [];
function checkPropertyIsRead(node: TSESTree.MemberExpression): boolean {
const topExpression: TSESTree.MemberExpression =
findTopMemberExpression(node);
switch (topExpression.parent?.type) {
case TSESTree.AST_NODE_TYPES.AssignmentExpression:
return topExpression.parent.right === topExpression;
default:
return true;
}
}
function findReadBeforeSync(): void {
const needSync: Set<Variable> = new Set<Variable>();
apiReferences.forEach((apiReference) => {
const operation = apiReference.operation;
const reference = apiReference.reference;
const variable = reference.resolved;
if (operation === "Get" && variable) {
needSync.add(variable);
}
if (operation === "Sync") {
needSync.clear();
}
if (operation === "Read" && variable && needSync.has(variable)) {
const node: TSESTree.Node = reference.identifier;
if (
node.parent?.type === TSESTree.AST_NODE_TYPES.MemberExpression &&
checkPropertyIsRead(node.parent)
) {
context.report({
node: node,
messageId: "callSync",
data: { name: node.name },
});
}
}
});
}
return {
Program(node) {
const scope = sourceCode.getScope
? sourceCode.getScope(node)
: context.getScope();
apiReferences = findOfficeApiReferences(scope);
apiReferences.sort((left, right) => {
return (
left.reference.identifier.range[1] -
right.reference.identifier.range[1]
);
});
findReadBeforeSync();
},
};
},
defaultOptions: [],
});
+19
View File
@@ -0,0 +1,19 @@
import callSyncAfterLoad from "./call-sync-after-load";
import callSyncBeforeRead from "./call-sync-before-read";
import loadObjectBeforeRead from "./load-object-before-read";
import noContextSyncInLoop from "./no-context-sync-in-loop";
import noEmptyLoad from "./no-empty-load";
import noNavigationalLoad from "./no-navigational-load";
import noOfficeInitialize from "./no-office-initialize";
import testForNullUsingIsNullObject from "./test-for-null-using-isNullObject";
export default {
"call-sync-before-read": callSyncBeforeRead,
"load-object-before-read": loadObjectBeforeRead,
"call-sync-after-load": callSyncAfterLoad,
"no-context-sync-in-loop": noContextSyncInLoop,
"no-empty-load": noEmptyLoad,
"no-navigational-load": noNavigationalLoad,
"no-office-initialize": noOfficeInitialize,
"test-for-null-using-isNullObject": testForNullUsingIsNullObject,
};
@@ -0,0 +1,146 @@
import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
import { Reference, Scope, Variable } from "@typescript-eslint/scope-manager";
import { isLoadCall, parsePropertiesArgument } from "../utils/load";
import { findCallExpression, findPropertiesRead } from "../utils/utils";
import { isGetFunction, isGetOrNullObjectFunction } from "../utils/getFunction";
export default ESLintUtils.RuleCreator(
() =>
"https://docs.microsoft.com/office/dev/add-ins/develop/application-specific-api-model#load",
)({
name: "load-object-before-read",
meta: {
type: "problem",
messages: {
loadBeforeRead:
"An explicit load call on '{{name}}' for property '{{loadValue}}' needs to be made before the property can be read.",
},
docs: {
description:
"Before you can read the properties of a proxy object, you must explicitly load the properties.",
},
schema: [],
},
create: function (context) {
const sourceCode = context.sourceCode ?? context.getSourceCode();
function isInsideWriteStatement(node: TSESTree.Node): boolean {
while (node.parent) {
node = node.parent;
if (node.type === TSESTree.AST_NODE_TYPES.AssignmentExpression)
return true;
}
return false;
}
function hasBeenLoaded(
node: TSESTree.Node,
loadLocation: Map<string, number>,
propertyName: string,
): boolean {
return (
loadLocation.has(propertyName) && // If reference came after load, return
node.range[1] > (loadLocation.get(propertyName) ?? 0)
);
}
function findLoadBeforeRead(scope: Scope) {
scope.variables.forEach((variable: Variable) => {
const loadLocation: Map<string, number> = new Map<string, number>();
let getFound: boolean = false;
variable.references.forEach((reference: Reference) => {
const node: TSESTree.Node = reference.identifier;
const parent = node.parent;
if (parent?.type === TSESTree.AST_NODE_TYPES.VariableDeclarator) {
getFound = false; // In case of reassignment
if (
parent.init &&
isGetFunction(parent.init) &&
!isGetOrNullObjectFunction(parent.init)
) {
getFound = true;
return;
}
}
if (parent?.type === TSESTree.AST_NODE_TYPES.AssignmentExpression) {
getFound = false; // In case of reassignment
if (
isGetFunction(parent.right) &&
!isGetOrNullObjectFunction(parent.right)
) {
getFound = true;
return;
}
}
if (!getFound) {
// If reference was not related to a previous get
return;
}
// Look for <obj>.load(...) call
if (parent?.type === TSESTree.AST_NODE_TYPES.MemberExpression) {
const methodCall = findCallExpression(parent);
if (methodCall && isLoadCall(methodCall)) {
const argument = methodCall.arguments[0];
const propertyNames: string[] = argument
? parsePropertiesArgument(argument)
: ["*"];
propertyNames.forEach((propertyName: string) => {
loadLocation.set(propertyName, node.range[1]);
});
return;
}
}
// Look for context.load(<obj>, "...") call
if (parent?.type === TSESTree.AST_NODE_TYPES.CallExpression) {
const args: TSESTree.CallExpressionArgument[] = parent?.arguments;
if (isLoadCall(parent) && args[0] == node && args.length < 3) {
const propertyNames: string[] = args[1]
? parsePropertiesArgument(args[1])
: ["*"];
propertyNames.forEach((propertyName: string) => {
loadLocation.set(propertyName, node.range[1]);
});
return;
}
}
const propertyName: string = findPropertiesRead(parent);
if (
!propertyName ||
hasBeenLoaded(node, loadLocation, propertyName) ||
hasBeenLoaded(node, loadLocation, "*") ||
isInsideWriteStatement(node)
) {
return;
}
context.report({
node: node,
messageId: "loadBeforeRead",
data: { name: node.name, loadValue: propertyName },
});
});
});
scope.childScopes.forEach(findLoadBeforeRead);
}
return {
Program(node) {
const scope = sourceCode.getScope
? sourceCode.getScope(node)
: context.getScope();
findLoadBeforeRead(scope);
},
};
},
defaultOptions: [],
});
@@ -0,0 +1,33 @@
import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
export default ESLintUtils.RuleCreator(
() =>
"https://docs.microsoft.com/office/dev/add-ins/concepts/correlated-objects-pattern",
)({
name: "no-context-sync-in-loop",
meta: {
type: "problem",
messages: {
loopedSync:
"Calling context.sync() inside a loop can lead to poor performance",
},
docs: {
description:
"Calling context.sync() inside of a loop dramatically increases the time the code runs, proportional to the number of iterations.",
},
schema: [],
},
create: function (context) {
return {
":matches(ForStatement, ForInStatement, WhileStatement, DoWhileStatement, ForOfStatement) CallExpression[callee.object.name='context'][callee.property.name='sync']"(
node: TSESTree.CallExpression,
) {
context.report({
node: node.callee,
messageId: "loopedSync",
});
},
};
},
defaultOptions: [],
});
+85
View File
@@ -0,0 +1,85 @@
import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
import { Reference, Scope, Variable } from "@typescript-eslint/scope-manager";
import { isGetFunction } from "../utils/getFunction";
import { parseLoadArguments, isLoadFunction } from "../utils/load";
export default ESLintUtils.RuleCreator(
() =>
"https://docs.microsoft.com/office/dev/add-ins/develop/application-specific-api-model#calling-load-without-parameters-not-recommended",
)({
name: "no-empty-load",
meta: {
type: "problem",
messages: {
emptyLoad: "Calling load without any argument slows down your add-in.",
},
docs: {
description:
"Calling load without any argument causes unneeded data to load and slows down your add-in.",
},
schema: [],
},
create: function (context) {
const sourceCode = context.sourceCode ?? context.getSourceCode();
function isEmptyLoad(node: TSESTree.MemberExpression): boolean {
if (isLoadFunction(node)) {
const propertyNames: string[] = parseLoadArguments(node);
if (propertyNames.length === 0) {
return true;
}
let foundEmptyProperty = false;
propertyNames.forEach((property: string) => {
if (!property) {
foundEmptyProperty = true;
}
});
return foundEmptyProperty;
}
return false;
}
function findEmptyLoad(scope: Scope) {
scope.variables.forEach((variable: Variable) => {
let getFound: boolean = false;
variable.references.forEach((reference: Reference) => {
const node: TSESTree.Node = reference.identifier;
if (reference.isWrite()) {
getFound = false; // In case of reassignment
if (reference.writeExpr && isGetFunction(reference.writeExpr)) {
getFound = true;
return;
}
}
if (!getFound) {
// If reference was not related to a previous get
return;
}
if (
node.parent?.type === TSESTree.AST_NODE_TYPES.MemberExpression &&
isEmptyLoad(node.parent)
) {
context.report({
node: node.parent,
messageId: "emptyLoad",
});
}
});
});
scope.childScopes.forEach(findEmptyLoad);
}
return {
Program(node) {
const scope = sourceCode.getScope
? sourceCode.getScope(node)
: context.getScope();
findEmptyLoad(scope);
},
};
},
defaultOptions: [],
});
@@ -0,0 +1,125 @@
import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
import { Reference, Scope, Variable } from "@typescript-eslint/scope-manager";
import { isGetFunction } from "../utils/getFunction";
import {
parseLoadArguments,
isLoadFunction,
parsePropertiesArgument,
} from "../utils/load";
import { getPropertyType, PropertyType } from "../utils/propertiesType";
export default ESLintUtils.RuleCreator(
() =>
"https://docs.microsoft.com/office/dev/add-ins/develop/application-specific-api-model#scalar-and-navigation-properties",
)({
name: "no-navigational-load",
meta: {
type: "problem",
messages: {
navigationalLoad:
"Calling load on the navigation property '{{loadValue}}' slows down your add-in.",
},
docs: {
description:
"Calling load on a navigation property causes unneeded data to load and slows down your add-in.",
},
schema: [],
},
create: function (context) {
const sourceCode = context.sourceCode ?? context.getSourceCode();
function isLoadingValidPropeties(propertyName: string): boolean {
const properties = propertyName.split("/");
const lastProperty = properties.pop();
if (!lastProperty) return false;
for (const property of properties) {
const propertyType = getPropertyType(property);
if (
propertyType !== PropertyType.navigational &&
propertyType !== PropertyType.ambiguous
) {
return false;
}
}
if (lastProperty === "*") {
return true;
}
const propertyType = getPropertyType(lastProperty);
return (
propertyType === PropertyType.scalar ||
propertyType === PropertyType.ambiguous
);
}
function findNavigationalLoad(scope: Scope) {
scope.variables.forEach((variable: Variable) => {
let getFound: boolean = false;
variable.references.forEach((reference: Reference) => {
const node: TSESTree.Node = reference.identifier;
if (reference.isWrite()) {
getFound = false; // In case of reassignment
if (reference.writeExpr && isGetFunction(reference.writeExpr)) {
getFound = true;
return;
}
}
if (!getFound) {
// If reference was not related to a previous get
return;
}
if (
node.parent?.type === TSESTree.AST_NODE_TYPES.MemberExpression &&
isLoadFunction(node.parent)
) {
// <obj>.load(...) call
const propertyNames: string[] = parseLoadArguments(node.parent);
propertyNames.forEach((propertyName: string) => {
if (propertyName && !isLoadingValidPropeties(propertyName)) {
context.report({
node: node.parent,
messageId: "navigationalLoad",
data: { name: node.name, loadValue: propertyName },
});
}
});
} else if (
node.parent?.type === TSESTree.AST_NODE_TYPES.CallExpression
) {
//context.load(<obj>, "...") call
const callee: TSESTree.MemberExpression = node.parent
.callee as TSESTree.MemberExpression;
const args: TSESTree.CallExpressionArgument[] =
node.parent.arguments;
if (isLoadFunction(callee) && args[0] == node && args.length < 3) {
const propertyNames: string[] = parsePropertiesArgument(args[1]);
propertyNames.forEach((propertyName: string) => {
if (propertyName && !isLoadingValidPropeties(propertyName)) {
context.report({
node: node.parent,
messageId: "navigationalLoad",
data: { name: node.name, loadValue: propertyName },
});
}
});
}
}
});
});
scope.childScopes.forEach(findNavigationalLoad);
}
return {
Program(node) {
const scope = sourceCode.getScope
? sourceCode.getScope(node)
: context.getScope();
findNavigationalLoad(scope);
},
};
},
defaultOptions: [],
});
@@ -0,0 +1,32 @@
import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
export default ESLintUtils.RuleCreator(
() =>
"https://docs.microsoft.com/office/dev/add-ins/develop/initialize-add-in#initialize-with-officeonready",
)({
name: "no-office-initialize",
meta: {
type: "suggestion",
messages: {
noOfficeInitialize:
"Office.onReady() is preferred over Office.initialize.",
},
docs: {
description: "Office.onReady() is more flexible than Office.initialize.",
},
schema: [],
},
create: function (context) {
return {
"AssignmentExpression[left.object.name='Office'][left.property.name='initialize']"(
node: TSESTree.AssignmentExpression,
) {
context.report({
node: node,
messageId: "noOfficeInitialize",
});
},
};
},
defaultOptions: [],
});
@@ -0,0 +1,155 @@
import {
ESLintUtils,
TSESTree,
AST_NODE_TYPES,
} from "@typescript-eslint/utils";
import { Reference, Scope, Variable } from "@typescript-eslint/scope-manager";
import { RuleFix, RuleFixer } from "@typescript-eslint/utils/ts-eslint";
import { isGetOrNullObjectFunction } from "../utils/getFunction";
export default ESLintUtils.RuleCreator(
() =>
"https://docs.microsoft.com/office/dev/add-ins/develop/application-specific-api-model#ornullobject-methods-and-properties",
)({
name: "test-for-null-using-isNullObject",
meta: {
type: "problem",
messages: {
useIsNullObject: "Test the isNullObject property of '{{name}}'.",
},
docs: {
description:
"Do not test the truthiness of an object returned by an OrNullObject method or property. Test it's isNullObject property.",
},
schema: [],
fixable: <"code" | "whitespace">"code",
},
create: function (context) {
const sourceCode = context.sourceCode ?? context.getSourceCode();
function isConditionalTestExpression(
node: TSESTree.Identifier | TSESTree.JSXIdentifier,
): boolean {
return (
node.parent != undefined &&
(node.parent.type === AST_NODE_TYPES.IfStatement ||
node.parent.type === AST_NODE_TYPES.WhileStatement ||
node.parent.type === AST_NODE_TYPES.DoWhileStatement ||
node.parent.type === AST_NODE_TYPES.ForStatement ||
node.parent.type === AST_NODE_TYPES.ConditionalExpression) &&
node === node.parent.test
);
}
function isInUnaryNullTest(
node: TSESTree.Identifier | TSESTree.JSXIdentifier,
): boolean {
return (
node.parent != undefined &&
node.parent.type === AST_NODE_TYPES.UnaryExpression &&
node.parent.operator === "!" &&
node.parent.argument === node
);
}
function isInBinaryNullTest(
node: TSESTree.Identifier | TSESTree.JSXIdentifier,
): boolean {
return (
node.parent != undefined &&
node.parent.type === AST_NODE_TYPES.BinaryExpression &&
((node.parent.left === node &&
node.parent.right.type === AST_NODE_TYPES.Literal &&
node.parent.right.raw === "null") ||
(node.parent.right === node &&
node.parent.left.type === AST_NODE_TYPES.Literal &&
node.parent.left.raw === "null"))
);
}
function isInNullTest(
node: TSESTree.Identifier | TSESTree.JSXIdentifier,
): boolean {
return (
isConditionalTestExpression(node) ||
node.parent?.type === AST_NODE_TYPES.LogicalExpression ||
isInUnaryNullTest(node) ||
isInBinaryNullTest(node)
);
}
function isNullObjectNode(node: TSESTree.Node | undefined): boolean {
if (
node &&
((node.type === AST_NODE_TYPES.VariableDeclarator &&
node.init &&
isGetOrNullObjectFunction(node.init) &&
node.id.type === AST_NODE_TYPES.Identifier) ||
(node.type === AST_NODE_TYPES.AssignmentExpression &&
isGetOrNullObjectFunction(node.right) &&
node.left.type === AST_NODE_TYPES.Identifier))
) {
return true;
}
return false;
}
function findNullObjectNullTests(scope: Scope): void {
const variables = scope.variables;
const childScopes = scope.childScopes;
for (let i = 0; i < variables.length; i++) {
const variable: Variable = variables[i];
const references: Reference[] = variable.references;
let nullObjectCall: boolean = false;
const nullTests: (TSESTree.Identifier | TSESTree.JSXIdentifier)[] = [];
for (let ref = 0; ref < references.length; ref++) {
const identifier: TSESTree.Identifier | TSESTree.JSXIdentifier =
references[ref].identifier;
if (isNullObjectNode(identifier.parent)) {
nullObjectCall = true;
}
if (isInNullTest(identifier)) {
nullTests.push(identifier);
}
}
if (nullObjectCall === true && nullTests.length > 0) {
nullTests.forEach((identifier) => {
context.report({
node: identifier,
messageId: "useIsNullObject",
data: { name: identifier.name },
fix: function (fixer: RuleFixer) {
let ruleFix: RuleFix;
if (isInBinaryNullTest(identifier) && identifier.parent) {
const newTest = identifier.name + ".isNullObject";
ruleFix = fixer.replaceText(identifier.parent, newTest);
} else {
ruleFix = fixer.insertTextAfter(identifier, ".isNullObject");
}
return ruleFix;
},
});
});
}
}
for (let i = 0; i < childScopes.length; ++i) {
findNullObjectNullTests(childScopes[i]);
}
}
return {
"Program:exit"(node) {
const scope = sourceCode.getScope
? sourceCode.getScope(node)
: context.getScope();
findNullObjectNullTests(scope);
},
};
},
defaultOptions: [],
});