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
+110
View File
@@ -0,0 +1,110 @@
import rules from "./rules";
import tsParser from "@typescript-eslint/parser";
import typescriptplugin from "@typescript-eslint/eslint-plugin";
import prettierplugin from "eslint-plugin-prettier";
import reactplugin from "eslint-plugin-react";
import eslintjs from "@eslint/js";
import eslintConfigPrettier from "eslint-config-prettier";
// eslint-disable-next-line @typescript-eslint/no-require-imports
const reactnativeplugin = require("eslint-plugin-react-native");
const plugin = {
meta: {
name: "eslint-plugin-office-addins",
version: "5.0.0",
},
rules,
configs: {},
};
const recommended = [
eslintjs.configs.recommended,
eslintConfigPrettier,
{
files: ["**/*.{js,mjs,cjs,ts,cts,mts}"],
plugins: {
"@typescript-eslint": typescriptplugin,
"office-addins": plugin,
prettier: prettierplugin,
},
languageOptions: {
parser: tsParser,
ecmaVersion: 6,
sourceType: "module",
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
rules: {
"@typescript-eslint/no-unused-vars": "error",
"no-delete-var": "warn",
"no-eval": "error",
"no-inner-declarations": "warn",
"no-octal": "warn",
"no-unused-vars": "off",
"office-addins/call-sync-after-load": "error",
"office-addins/call-sync-before-read": "error",
"office-addins/load-object-before-read": "error",
"office-addins/no-context-sync-in-loop": "warn",
"office-addins/no-empty-load": "warn",
"office-addins/no-navigational-load": "warn",
"office-addins/no-office-initialize": "warn",
"office-addins/test-for-null-using-isNullObject": "error",
"prettier/prettier": ["error", { endOfLine: "auto" }],
},
},
];
const react = [
reactplugin.configs.flat.recommended,
...recommended,
{
plugins: {
"office-addins": plugin,
react: reactplugin,
},
settings: {
react: {
version: "detect",
},
},
},
];
const reactnative = [
reactnativeplugin.configs.all,
...recommended,
{
plugins: {
"office-addins": plugin,
react: reactnativeplugin,
},
settings: {
react: {
version: "detect",
},
},
},
];
const test = [
...recommended,
{
plugins: {
"office-addins": plugin,
},
rules: {},
},
];
Object.assign(plugin.configs, {
recommended,
react,
reactnative,
test,
});
module.exports = plugin;
@@ -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: [],
});
@@ -0,0 +1,148 @@
{
"getFunctions": [
"getAbsoluteResizedRange",
"getActiveCell",
"getActiveChart",
"getActiveNotebook",
"getActiveOutline",
"getActivePage",
"getActiveParagraph",
"getActiveSection",
"getActiveSlicer",
"getActiveWorksheet",
"getAncestor",
"getAsImage",
"getBase64Image",
"getBase64ImageSrc",
"getBorder",
"getBoundingRect",
"getById",
"getByName",
"getByNamespace",
"getByTag",
"getByTitle",
"getByTypes",
"getCell",
"getCellPadding",
"getCellProperties",
"getColumn",
"getColumnLabelRange",
"getColumnProperties",
"getColumnsAfter",
"getColumnsBefore",
"getCount",
"getDataBodyRange",
"getDataCommonPostprocess",
"getDataHierarchy",
"getDefault",
"getDescendants",
"getDimensionValues",
"getDirectPrecedents",
"getDocument",
"getEntireColumn",
"getEntireRow",
"getExtendedRange",
"getFilterAxisRange",
"getFirst",
"getFooter",
"getHeader",
"GetHeaderRowRange",
"getHtml",
"getHyperlinkRanges",
"getImage",
"getIntersection",
"getInvalidCells",
"getIsActiveCollabSession",
"getItem",
"getItemAt",
"getLast",
"getLastCell",
"getLastColumn",
"getLastRow",
"getLevelParagraphs",
"getLevelString",
"getLocation",
"getMergedAreas",
"getNext",
"getNextTextRange",
"getOffsetRange",
"getOffsetRangeAreas",
"getOoxml",
"getParagraphAfter",
"getParagraphBefore",
"getParagraphInfo",
"getParentComment",
"getPivotItems",
"getPivotTables",
"getPrevious",
"getPrintArea",
"getPrintTitleColumns",
"getPrintTitleRows",
"getRange",
"getRangeAreasBySheet",
"getRangeByIndexes",
"getRangeEdge",
"getRanges",
"getReferenceId",
"getResizedRange",
"getRestApiId",
"getRow",
"getRowLabelRange",
"getRowProperties",
"getRowsAbove",
"getRowsBelow",
"getSelectedRange",
"getSelectedRanges",
"getSelection",
"getSpecialCells",
"getSpillingToRange",
"getSpillParent",
"GetStencilInfo",
"getSubstring",
"getSurroundingRegion",
"getTables",
"getText",
"getTextRanges",
"getTotalRowRange",
"getUsedRange",
"getUsedRangeAreas",
"getVisibleView",
"getWindowSize",
"getXml"
],
"getOrNullObjectFunctions": [
"getActiveChartOrNullObject",
"getActiveNotebookOrNull",
"getActiveOutlineOrNull",
"getActivePageOrNull",
"getActiveParagraphOrNull",
"getActiveSectionOrNull",
"getActiveSlicerOrNullObject",
"getAncestorOrNullObject",
"getByIdOrNullObject",
"getCellOrNullObject",
"getFirstOrNullObject",
"getIntersectionOrNullObject",
"getIntersectionOrNullObject",
"getInvalidCellsOrNullObject",
"getItemOrNullObject",
"getLastOrNullObject",
"getLocationOrNullObject",
"getNextOrNullObject",
"getNextTextRangeOrNullObject",
"getParagraphAfterOrNullObject",
"getParagraphBeforeOrNullObject",
"getPreviousOrNullObject",
"getPrintAreaOrNullObject",
"getPrintTitleColumnsOrNullObject",
"getPrintTitleRowsOrNullObject",
"getRangeAreasOrNullObjectBySheet",
"getRangeOrNullObject",
"getSpecialCellsOrNullObject",
"getSpecialCellsOrNullObject",
"getSpillingToRangeOrNullObject",
"getSpillParentOrNullObject",
"getUsedRangeAreasOrNullObject",
"getUsedRangeOrNullObject"
]
}
+485
View File
@@ -0,0 +1,485 @@
{
"navigational": [
"_V1Api",
"application",
"areas",
"arrayValues",
"autoFilter",
"axes",
"beginConnectedShape",
"binOptions",
"bindings",
"border",
"borders",
"bottom",
"boxwhiskerOptions",
"categoryAxis",
"cellValue",
"cellValueOrNullObject",
"charts",
"colorScale",
"colorScaleOrNullObject",
"columnHierarchies",
"columns",
"conditionalFormats",
"cultureInfo",
"custom",
"customOrNullObject",
"customProperties",
"customXmlParts",
"dataBar",
"dataBarOrNullObject",
"dataConnections",
"dataHierarchies",
"dataLabel",
"dataLabels",
"dataValidation",
"datetimeFormat",
"defaultForAllPages",
"endConnectedShape",
"evenPages",
"field",
"fill",
"filter",
"filterHierarchies",
"firstPage",
"font",
"freezePanes",
"functions",
"geometricShape",
"group",
"headersFooters",
"hierarchies",
"horizontalPageBreaks",
"iconSet",
"iconSetOrNullObject",
"image",
"items",
"iterativeCalculation",
"label",
"layout",
"legend",
"legendEntries",
"line",
"lineFormat",
"majorGridlines",
"mapOptions",
"minorGridlines",
"namedSheetViews",
"names",
"negativeFormat",
"oddPages",
"pageLayout",
"parentGroup",
"pivotOptions",
"pivotStyle",
"pivotTableStyles",
"pivotTables",
"plotArea",
"points",
"positiveFormat",
"preset",
"presetOrNullObject",
"properties",
"protection",
"ranges",
"replies",
"right",
"rowHierarchies",
"rows",
"series",
"seriesAxis",
"settings",
"shape",
"shapes",
"slicerItems",
"slicerStyle",
"slicerStyles",
"slicers",
"sort",
"styles",
"tableStyle",
"tableStyles",
"tables",
"textComparison",
"textComparisonOrNullObject",
"textFrame",
"textRange",
"timelineStyles",
"topBottom",
"topBottomOrNullObject",
"trendlines",
"valueAxis",
"verticalPageBreaks",
"worksheet",
"worksheetOrNullObject",
"worksheets",
"xErrorBars",
"yErrorBars"
],
"scalar": [
"_Id",
"address",
"addressLocal",
"addresses",
"alignment",
"allowMultipleFiltersPerField",
"allowOverflow",
"allowUnderflow",
"altTextDescription",
"altTextTitle",
"areaCount",
"author",
"authorEmail",
"authorName",
"autoFormat",
"autoIndent",
"autoSave",
"autoSizeSetting",
"autoText",
"axisColor",
"axisFormat",
"axisGroup",
"backwardPeriod",
"barDirection",
"baseTimeUnit",
"beginArrowheadLength",
"beginArrowheadStyle",
"beginArrowheadWidth",
"beginConnectedSite",
"blackAndWhite",
"bold",
"borderColor",
"bottomMargin",
"bubbleScale",
"builtIn",
"calculationEngineVersion",
"calculationMode",
"calculationState",
"caption",
"category",
"categoryLabelLevel",
"categoryType",
"cellAddresses",
"cellCount",
"centerFooter",
"centerHeader",
"centerHorizontally",
"centerVertically",
"chartDataPointTrack",
"chartType",
"color",
"colorScheme",
"columnCount",
"columnHidden",
"columnIndex",
"columnWidth",
"comment",
"company",
"connectionSiteCount",
"connectorType",
"content",
"contentType",
"count",
"creationDate",
"criteria",
"customDisplayUnit",
"dashStyle",
"dateSeparator",
"decimalSeparator",
"displayBlanksAs",
"displayUnit",
"doughnutHoleSize",
"draftMode",
"emptyCellText",
"enableCalculation",
"enableDataValueEditing",
"enableEvents",
"enableFieldList",
"enableMultipleFilterItems",
"enabled",
"endArrowheadLength",
"endArrowheadStyle",
"endArrowheadWidth",
"endConnectedSite",
"endStyleCap",
"errorAlert",
"explosion",
"fillColor",
"fillEmptyCells",
"filtered",
"firstPageNumber",
"firstSliceAngle",
"footerMargin",
"foregroundColor",
"formula",
"formulaHidden",
"formulaLocal",
"formulaR1C1",
"formulas",
"formulasLocal",
"formulasR1C1",
"forwardPeriod",
"gapWidth",
"geometricShapeType",
"gradientFill",
"gradientMaximumColor",
"gradientMaximumType",
"gradientMaximumValue",
"gradientMidpointColor",
"gradientMidpointType",
"gradientMidpointValue",
"gradientMinimumColor",
"gradientMinimumType",
"gradientMinimumValue",
"gradientStyle",
"hasData",
"hasDataLabel",
"hasDataLabels",
"hasSpill",
"hasText",
"headerMargin",
"height",
"hidden",
"highlightFirstColumn",
"highlightLastColumn",
"horizontalAlignment",
"horizontalOverflow",
"hyperlink",
"id",
"ignoreBlanks",
"include",
"includeAlignment",
"includeBorder",
"includeFont",
"includeNumber",
"includePatterns",
"includeProtection",
"indentLevel",
"index",
"insideHeight",
"insideLeft",
"insideTop",
"insideWidth",
"intercept",
"invertColor",
"invertIfNegative",
"isBeginConnected",
"isBetweenCategories",
"isDataFiltered",
"isDirty",
"isEndConnected",
"isEntireColumn",
"isEntireRow",
"isExpanded",
"isFilterCleared",
"isSelected",
"italic",
"key",
"keywords",
"labelStrategy",
"lastAuthor",
"layoutType",
"leftFooter",
"leftHeader",
"leftMargin",
"legacyId",
"level",
"lineStyle",
"linkNumberFormat",
"linkedDataTypeState",
"lockAspectRatio",
"locked",
"logBase",
"longDatePattern",
"longTimePattern",
"lowerBoundRule",
"majorTickMark",
"majorTimeUnitScale",
"majorUnit",
"manager",
"markerBackgroundColor",
"markerForegroundColor",
"markerSize",
"markerStyle",
"matchCase",
"matchPositiveBorderColor",
"matchPositiveFillColor",
"maxChange",
"maxIteration",
"maximum",
"mentions",
"method",
"minimum",
"minorTickMark",
"minorTimeUnitScale",
"minorUnit",
"movingAveragePeriod",
"multiLevel",
"name",
"namespaceUri",
"numberDecimalSeparator",
"numberFormatCategories",
"numberFormatLocal",
"numberGroupSeparator",
"offset",
"options",
"orientation",
"overflowValue",
"overlap",
"overlay",
"paperSize",
"parentLabelStrategy",
"pattern",
"patternColor",
"patternTintAndShade",
"placement",
"plotBy",
"plotOrder",
"plotVisibleOnly",
"polynomialOrder",
"position",
"positionAt",
"preserveFormatting",
"previouslySaved",
"printComments",
"printErrors",
"printGridlines",
"printHeadings",
"printOrder",
"priority",
"projectionType",
"prompt",
"protected",
"quartileCalculation",
"readOnly",
"readingOrder",
"refreshOnOpen",
"removed",
"resolved",
"reverseIconOrder",
"reversePlotOrder",
"revisionNumber",
"richContent",
"rightFooter",
"rightHeader",
"rightMargin",
"rotation",
"roundedCorners",
"rowCount",
"rowHeight",
"rowHidden",
"rowIndex",
"savedAsArray",
"scaleType",
"scope",
"secondPlotSize",
"separator",
"seriesNameLevel",
"shortDatePattern",
"showAllFieldButtons",
"showAllItems",
"showAs",
"showAxisFieldButtons",
"showBandedColumns",
"showBandedRows",
"showBubbleSize",
"showCategoryName",
"showColumnGrandTotals",
"showConnectorLines",
"showDataBarOnly",
"showDataLabelsOverMaximum",
"showDisplayUnitLabel",
"showEquation",
"showFieldHeaders",
"showFilterButton",
"showGridlines",
"showHeaders",
"showHeadings",
"showIconOnly",
"showInnerPoints",
"showLeaderLines",
"showLegendFieldButtons",
"showLegendKey",
"showMeanLine",
"showMeanMarker",
"showOutlierPoints",
"showPercentage",
"showRSquared",
"showReportFilterFieldButtons",
"showRowGrandTotals",
"showSeriesName",
"showShadow",
"showTotals",
"showValue",
"showValueFieldButtons",
"shrinkToFit",
"sideIndex",
"size",
"smooth",
"sortBy",
"splitType",
"splitValue",
"standardHeight",
"standardWidth",
"state",
"stopIfTrue",
"strikethrough",
"style",
"subject",
"subscript",
"subtotalLocation",
"subtotals",
"summarizeBy",
"superscript",
"tabColor",
"text",
"textOrientation",
"thousandsSeparator",
"threeColorScale",
"tickLabelPosition",
"tickLabelSpacing",
"tickMarkSpacing",
"timeSeparator",
"tintAndShade",
"topMargin",
"transparency",
"type",
"types",
"underflowValue",
"underline",
"uniqueRemaining",
"upperBoundRule",
"useCustomSortLists",
"usePrecisionAsDisplayed",
"useSheetMargins",
"useSheetScale",
"useStandardHeight",
"useStandardWidth",
"useSystemSeparators",
"valid",
"value",
"valueTypes",
"values",
"varyByCategories",
"verticalAlignment",
"verticalOverflow",
"visibility",
"visible",
"weight",
"width",
"wrapText",
"zOrderPosition",
"zoom"
],
"ambiguous": [
"comments",
"fields",
"format",
"left",
"numberFormat",
"rule",
"title",
"top"
]
}
+26
View File
@@ -0,0 +1,26 @@
import { TSESTree } from "@typescript-eslint/utils";
import * as getJson from "./data/getFunctions.json";
const getFunctions: Set<string> = new Set<string>(getJson.getFunctions);
const getOrNullObjectFunctions: Set<string> = new Set<string>(
getJson.getOrNullObjectFunctions,
);
export function isGetFunction(node: TSESTree.Node): boolean {
return (
node.type == TSESTree.AST_NODE_TYPES.CallExpression &&
node.callee.type === TSESTree.AST_NODE_TYPES.MemberExpression &&
node.callee.property.type === TSESTree.AST_NODE_TYPES.Identifier &&
(getFunctions.has(node.callee.property.name) ||
getOrNullObjectFunctions.has(node.callee.property.name))
);
}
export function isGetOrNullObjectFunction(node: TSESTree.Node): boolean {
return (
node.type == TSESTree.AST_NODE_TYPES.CallExpression &&
node.callee.type === TSESTree.AST_NODE_TYPES.MemberExpression &&
node.callee.property.type === TSESTree.AST_NODE_TYPES.Identifier &&
getOrNullObjectFunctions.has(node.callee.property.name)
);
}
+117
View File
@@ -0,0 +1,117 @@
import { AST_NODE_TYPES, TSESTree } from "@typescript-eslint/utils";
import { findCallExpression } from "./utils";
export function isLoadFunction(node: TSESTree.MemberExpression): boolean {
const methodCall = findCallExpression(node);
return methodCall !== undefined && isLoadCall(methodCall);
}
export function isLoadCall(node: TSESTree.CallExpression): boolean {
return (
node &&
node.callee.type === AST_NODE_TYPES.MemberExpression &&
node.callee.property.type === AST_NODE_TYPES.Identifier &&
node.callee.property.name === "load"
);
}
export function isLoadReference(
node: TSESTree.Identifier | TSESTree.JSXIdentifier,
) {
return (
node.parent &&
node.parent.type === TSESTree.AST_NODE_TYPES.MemberExpression &&
isLoadFunction(node.parent)
);
}
export function isContextLoadArgumentReference(
node: TSESTree.Identifier | TSESTree.JSXIdentifier,
) {
return (
node.parent?.type === AST_NODE_TYPES.CallExpression &&
node.parent.callee.type === AST_NODE_TYPES.MemberExpression &&
node.parent.callee.object.type === AST_NODE_TYPES.Identifier &&
node.parent.callee.object.name === "context" &&
node.parent.callee.property.type === AST_NODE_TYPES.Identifier &&
node.parent.callee.property.name === "load"
);
}
function parseObjectExpressionProperty(
objectExpression: TSESTree.ObjectExpression,
): string[] {
let composedProperties: string[] = [];
objectExpression.properties.forEach((property) => {
if (
property.type === AST_NODE_TYPES.Property &&
property.key.type === AST_NODE_TYPES.Identifier
) {
const propertyName: string = property.key.name;
if (property.value.type === AST_NODE_TYPES.ObjectExpression) {
const composedProperty = parseObjectExpressionProperty(property.value);
if (composedProperty.length !== 0) {
composedProperties = composedProperties.concat(
propertyName + "/" + composedProperty,
);
}
} else if (
property.value.type === AST_NODE_TYPES.Literal &&
property.value.value // Checking if the value assigined to the property is true
) {
composedProperties = composedProperties.concat(propertyName);
}
}
});
return composedProperties;
}
function parseLoadStringArgument(argument: string): string[] {
const properties: string[] = [];
argument
.replace(/\s/g, "")
.split(",")
.forEach((property: string) => {
properties.push(property);
});
return properties;
}
export function parseLoadArguments(node: TSESTree.MemberExpression): string[] {
const methodCall = findCallExpression(node);
if (methodCall && isLoadCall(methodCall)) {
const argument = methodCall.arguments[0];
if (!argument) {
return [];
}
return parsePropertiesArgument(argument);
}
throw new Error("error in parseLoadArgument function.");
}
export function parsePropertiesArgument(
argument: TSESTree.CallExpressionArgument,
): string[] {
let properties: string[] = [];
if (argument.type === AST_NODE_TYPES.ArrayExpression) {
argument.elements.forEach((element) => {
if (element != null && element.type === TSESTree.AST_NODE_TYPES.Literal) {
properties = properties.concat(
parseLoadStringArgument(element.value as string),
);
}
});
} else if (argument.type === TSESTree.AST_NODE_TYPES.Literal) {
properties = parseLoadStringArgument(argument.value as string);
} else if (argument.type === TSESTree.AST_NODE_TYPES.ObjectExpression) {
properties = parseObjectExpressionProperty(argument);
}
return properties;
}
+28
View File
@@ -0,0 +1,28 @@
import * as propertiesJson from "./data/properties.json";
export enum PropertyType {
navigational,
scalar,
ambiguous, // Can be scalar or navigational. Depends of the context.
notProperty,
}
const navigationProperties: Set<string> = new Set<string>(
propertiesJson.navigational,
);
const scalarProperties: Set<string> = new Set<string>(propertiesJson.scalar);
const ambiguousProperties: Set<string> = new Set<string>(
propertiesJson.ambiguous,
);
export function getPropertyType(propertyName: string): PropertyType {
if (navigationProperties.has(propertyName)) {
return PropertyType.navigational;
} else if (scalarProperties.has(propertyName)) {
return PropertyType.scalar;
} else if (ambiguousProperties.has(propertyName)) {
return PropertyType.ambiguous;
} else {
return PropertyType.notProperty;
}
}
+127
View File
@@ -0,0 +1,127 @@
import { AST_NODE_TYPES, TSESTree } from "@typescript-eslint/utils";
import { Reference, Scope, Variable } from "@typescript-eslint/scope-manager";
import { isGetFunction } from "./getFunction";
import { isContextLoadArgumentReference, isLoadReference } from "./load";
function isContextSyncIdentifier(
node: TSESTree.Identifier | TSESTree.JSXIdentifier,
): boolean {
return (
node.name === "context" &&
node.parent?.type === AST_NODE_TYPES.MemberExpression &&
node.parent.parent?.type === AST_NODE_TYPES.CallExpression &&
node.parent.property.type === AST_NODE_TYPES.Identifier &&
node.parent.property.name === "sync"
);
}
export function findTopMemberExpression(
node: TSESTree.MemberExpression,
): TSESTree.MemberExpression {
while (node.parent && node.parent.type === AST_NODE_TYPES.MemberExpression) {
node = node.parent;
}
return node;
}
export function findCallExpression(
node: TSESTree.MemberExpression,
): TSESTree.CallExpression | undefined {
while (node.parent && node.parent.type === AST_NODE_TYPES.MemberExpression) {
node = node.parent;
}
if (node.parent?.type === AST_NODE_TYPES.CallExpression) {
return node.parent;
}
return undefined;
}
export type OfficeApiReference = {
/**
* Get: An OfficeJs object, which is created when calling a `get` type function
* Load: A reference to `object.load()` type call
* Method: The reference is calling a method. Ex: `object.methodCall()`
* Read: The reference value is being read and it is not a method
* Sync: A call to `context.sync()`
*/
operation: "Get" | "Load" | "Method" | "Read" | "Sync";
reference: Reference;
};
let proxyVariables: Set<Variable>;
let apiReferences: OfficeApiReference[];
export function findOfficeApiReferences(scope: Scope): OfficeApiReference[] {
proxyVariables = new Set<Variable>();
apiReferences = [];
findOfficeApiReferencesInScope(scope);
return apiReferences;
}
function findOfficeApiReferencesInScope(scope: Scope): void {
scope.references.forEach((reference) => {
const node: TSESTree.Node = reference.identifier;
if (
reference.isWrite() &&
reference.writeExpr &&
isGetFunction(reference.writeExpr) &&
reference.resolved
) {
proxyVariables.add(reference.resolved);
apiReferences.push({ operation: "Get", reference: reference });
} else if (isContextSyncIdentifier(reference.identifier)) {
apiReferences.push({ operation: "Sync", reference: reference });
} else if (
reference.isRead() &&
reference.resolved &&
proxyVariables.has(reference.resolved)
) {
if (isLoadReference(node)) {
// <obj>.load(...)
apiReferences.push({ operation: "Load", reference: reference });
} else if (isContextLoadArgumentReference(node)) {
// context.load(<obj>, ...)
apiReferences.push({ operation: "Load", reference: reference });
} else if (isMethodReference(node)) {
apiReferences.push({ operation: "Method", reference: reference });
} else {
apiReferences.push({ operation: "Read", reference: reference });
}
}
});
scope.childScopes.forEach(findOfficeApiReferencesInScope);
}
function isMethod(node: TSESTree.MemberExpression): boolean {
const topExpression: TSESTree.MemberExpression =
findTopMemberExpression(node);
return (
topExpression.parent?.type === AST_NODE_TYPES.CallExpression &&
topExpression.parent.callee === topExpression
);
}
function isMethodReference(node: TSESTree.Identifier | TSESTree.JSXIdentifier) {
return (
node.parent &&
node.parent.type === TSESTree.AST_NODE_TYPES.MemberExpression &&
isMethod(node.parent)
);
}
export function findPropertiesRead(node: TSESTree.Node | undefined): string {
let propertyName = ""; // Will be a string combined with '/' for the case of navigation properties
while (node) {
if (
node.type === AST_NODE_TYPES.MemberExpression &&
node.property.type === AST_NODE_TYPES.Identifier &&
!isMethod(node)
) {
propertyName += node.property.name + "/";
}
node = node.parent;
}
return propertyName.slice(0, -1);
}