Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/mcp-provider-mobile-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"eslint": "^9.35.0",
"dedent": "^1.5.3",
"@salesforce/mcp-provider-api": "^0.4.0",
"@salesforce/eslint-plugin-lwc-graph-analyzer": "^1.0.0",
"@salesforce/eslint-plugin-lwc-graph-analyzer": "^1.1.0-beta.2",
"zod": "^3.25.76"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const CodeAnalysisBaseIssueSchema = z.object({
});

export const CodeAnalysisIssueSchema = CodeAnalysisBaseIssueSchema.extend({
filePath: z.string().describe('The relative path to the file where the issue occurs'),
code: z.string().optional().describe('What is the code snippet with the issue?'),
location: z
.object({
Expand Down
6 changes: 3 additions & 3 deletions packages/mcp-provider-mobile-web/src/schemas/lwcSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ const LwcFileSchema = z.object({
export const LwcCodeSchema = z.object({
name: z.string().min(1).describe('Name of the LWC component'),
namespace: z.string().describe('Namespace of the LWC component').default('c'),
js: LwcFileSchema.describe('LWC component JavaScript file.'),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LWC has only 1 JS file

html: z.array(LwcFileSchema).min(1).describe('LWC component HTML templates.'),
js: z.array(LwcFileSchema).min(1).describe('LWC component JavaScript files.'),
css: z.array(LwcFileSchema).optional().describe('LWC component CSS files.'),
jsMetaXml: LwcFileSchema.describe('LWC component configuration .js-meta.xml file.'),
jsMetaXml: LwcFileSchema.optional().describe('LWC component configuration .js-meta.xml file.'),
});

export type LwcCodeType = z.TypeOf<typeof LwcCodeSchema>;
export type LwcCodeType = z.infer<typeof LwcCodeSchema>;
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import { McpTool, type McpToolConfig } from '@salesforce/mcp-provider-api';
import { ReleaseState, Toolset } from '@salesforce/mcp-provider-api';
import { LwcCodeSchema, type LwcCodeType } from '../../schemas/lwcSchema.js';
import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
import lwcGraphAnalyzerPlugin from '@salesforce/eslint-plugin-lwc-graph-analyzer';
import { ruleConfigs } from './ruleConfig.js';

Expand All @@ -34,6 +33,7 @@ import {
const ANALYSIS_EXPERT_NAME = 'Mobile Web Offline Analysis';
const PLUGIN_NAME = '@salesforce/lwc-graph-analyzer';
const RECOMMENDED_CONFIG = lwcGraphAnalyzerPlugin.configs.recommended;
const { bundleAnalyzer } = lwcGraphAnalyzerPlugin.processors;

const LINTER_CONFIG: Linter.Config = {
name: `config: ${PLUGIN_NAME}`,
Expand All @@ -45,7 +45,6 @@ const LINTER_CONFIG: Linter.Config = {

type InputArgsShape = typeof LwcCodeSchema.shape;
type OutputArgsShape = typeof ExpertsCodeAnalysisIssuesSchema.shape;
type InputArgs = z.infer<typeof LwcCodeSchema>;

export class OfflineAnalysisTool extends McpTool<InputArgsShape, OutputArgsShape> {
private readonly linter: Linter;
Expand Down Expand Up @@ -82,7 +81,7 @@ export class OfflineAnalysisTool extends McpTool<InputArgsShape, OutputArgsShape
};
}

public async exec(args: InputArgs): Promise<CallToolResult> {
public async exec(args: LwcCodeType): Promise<CallToolResult> {
try {
const analysisResults = await this.analyzeCode(args);

Expand Down Expand Up @@ -119,22 +118,42 @@ export class OfflineAnalysisTool extends McpTool<InputArgsShape, OutputArgsShape
}

public async analyzeCode(code: LwcCodeType): Promise<ExpertsCodeAnalysisIssuesType> {
const jsCode = code.js.map((js: { content: string }) => js.content).join('\n');
const { messages } = this.linter.verifyAndFix(jsCode, LINTER_CONFIG, {
fix: true,
});
let offlineAnalysisIssues: ExpertCodeAnalysisIssuesType;

if (!code.js) {
offlineAnalysisIssues = {
expertReviewerName: ANALYSIS_EXPERT_NAME,
issues: [],
};
} else {
const jsCode = code.js.content;
const jsPath = code.js.path;

const htmlCodes = code.html.length > 0 ? code.html.map((html) => html.content) : ([''] as string[]);

const baseName = code.name;

bundleAnalyzer.setLwcBundleFromContent(baseName, jsCode, ...htmlCodes);

const { messages } = this.linter.verifyAndFix(jsCode, LINTER_CONFIG, {
fix: true,
filename: `${baseName}.js`,
});

offlineAnalysisIssues = this.analyzeIssues(jsCode, messages, jsPath);
}

const offlineAnalysisIssues = this.analyzeIssues(jsCode, messages);
return {
analysisResults: [offlineAnalysisIssues],
orchestrationInstructions: this.getOrchestrationInstructions(),
};
}

private getOrchestrationInstructions(): string {
return ExpertsCodeAnalysisIssuesSchema.shape.orchestrationInstructions.parse(undefined);
}

private analyzeIssues(code: string, messages: Linter.LintMessage[]): ExpertCodeAnalysisIssuesType {
private analyzeIssues(code: string, messages: Linter.LintMessage[], jsPath: string): ExpertCodeAnalysisIssuesType {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add the file path to help the model quickly locate the file containing the issues.

const issues: CodeAnalysisIssueType[] = [];

for (const violation of messages) {
Expand All @@ -144,6 +163,7 @@ export class OfflineAnalysisTool extends McpTool<InputArgsShape, OutputArgsShape

if (ruleReviewer) {
const issue: CodeAnalysisIssueType = {
filePath: jsPath,
type: ruleReviewer.type,
description: ruleReviewer.description,
intentAnalysis: ruleReviewer.intentAnalysis,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export interface RuleConfig {
id: string; //ESLint rule id
config: CodeAnalysisBaseIssueType;
}
// ********** Rules: no-private-wire-config-property **********
// ********** Rule: no-private-wire-config-property **********
const NO_PRIVATE_WIRE_CONFIG_RULE_ID = '@salesforce/lwc-graph-analyzer/no-private-wire-config-property';

const noPrivateWireRule: CodeAnalysisBaseIssueType = {
Expand All @@ -37,7 +37,7 @@ const noPrivateWireRule: CodeAnalysisBaseIssueType = {
`,
};

// ********** Rules: no-wire-config-references-non-local-property-reactive-value **********
// ********** Rule: no-wire-config-references-non-local-property-reactive-value **********
const NO_WIRE_CONFIG_REFERENCES_NON_LOCAL_PROPERTY_REACTIVE_VALUE_RULE_ID =
'@salesforce/lwc-graph-analyzer/no-wire-config-references-non-local-property-reactive-value';

Expand Down Expand Up @@ -74,4 +74,92 @@ const noWireConfigReferenceNonLocalPropertyRuleConfig: RuleConfig = {
config: noWireConfigReferenceNonLocalPropertyRule,
};

export const ruleConfigs: RuleConfig[] = [noPrivateWireRuleConfig, noWireConfigReferenceNonLocalPropertyRuleConfig];
// ********** getter related violations **********
const getterViolation: CodeAnalysisBaseIssueType = {
type: 'Violations in Getter',
description: 'A getter method does more than just returning a value',
intentAnalysis:
'The developer attempted to modify component state, prepare data for consumption, or reference functions within a getter function.',
suggestedAction: dedent`
# Compliant getter implementations

Getters that:
- Directly access and return property values
- Return a literal value
- Compute and return values derived from existing properties

# Non-compliant getter implementations

## Violation: getters that call functions

Getters that call functions cannot be primed for offline use cases.

### Remediation

Reorganize any getter implementation code that calls a function, to move such calls out of the getter. Avoid invoking any function calls within getters.

## Violation: getters with side effects

Getters that assign values to member variables or modify state create unpredictable side effects and are not suitable for offline scenarios.

### Remediation

Never assign values to member variables within a getter. LWC getters should only retrieve data without modifying any state. If you need to compute and cache a value, perform the computation and assignment in a lifecycle hook or method, then have the getter simply return the cached value.

## Violation: getters that do more than just return a value

Getters that perform complex operations beyond returning a value cannot be primed for offline use cases.

### Remediation

Review the getters and make sure that they're composed to only return a value. Move any complex logic, data processing, or multiple operations into separate methods or lifecycle hooks, and have the getter simply return the result.
`,
};

// ********** Rule: no-assignment-expression-assigns-value-to-member-variable **********
const NO_ASSIGNMENT_EXPRESSION_ASSIGNS_VALUE_TO_MEMBER_VARIABLE_RULE_ID =
'@salesforce/lwc-graph-analyzer/no-assignment-expression-assigns-value-to-member-variable';
const noAssignmentExpressionAssignsValueToMemberVariableRuleConfig: RuleConfig = {
id: NO_ASSIGNMENT_EXPRESSION_ASSIGNS_VALUE_TO_MEMBER_VARIABLE_RULE_ID,
config: getterViolation,
};

// ********** Rule: no-reference-to-class-functions **********
const NO_REFERENCE_TO_CLASS_FUNCTIONS_RULE_ID = '@salesforce/lwc-graph-analyzer/no-reference-to-class-functions';
const noReferenceToClassFunctionsRuleConfig: RuleConfig = {
id: NO_REFERENCE_TO_CLASS_FUNCTIONS_RULE_ID,
config: getterViolation,
};

// ********** Rule: no-reference-to-module-functions **********
const NO_REFERENCE_TO_MODULE_FUNCTIONS_RULE_ID = '@salesforce/lwc-graph-analyzer/no-reference-to-module-functions';
const noReferenceToModuleFunctionsRuleConfig: RuleConfig = {
id: NO_REFERENCE_TO_MODULE_FUNCTIONS_RULE_ID,
config: getterViolation,
};

// ********** Rule: no-getter-contains-more-than-return-statement **********
const NO_GETTER_CONTAINS_MORE_THAN_RETURN_STATEMENT_RULE_ID =
'@salesforce/lwc-graph-analyzer/no-getter-contains-more-than-return-statement';
const noGetterContainsMoreThanReturnStatementRuleConfig: RuleConfig = {
id: NO_GETTER_CONTAINS_MORE_THAN_RETURN_STATEMENT_RULE_ID,
config: getterViolation,
};

// ********** Rule: no-unsupported-member-variable-in-member-expression **********
const NO_UNSUPPORTED_MEMBER_VARIABLE_IN_MEMBER_EXPRESSION_RULE_ID =
'@salesforce/lwc-graph-analyzer/no-unsupported-member-variable-in-member-expression';
const noUnsupportedMemberVariableInMemberExpressionRuleConfig: RuleConfig = {
id: NO_UNSUPPORTED_MEMBER_VARIABLE_IN_MEMBER_EXPRESSION_RULE_ID,
config: getterViolation,
};

export const ruleConfigs: RuleConfig[] = [
noPrivateWireRuleConfig,
noWireConfigReferenceNonLocalPropertyRuleConfig,
noAssignmentExpressionAssignsValueToMemberVariableRuleConfig,
noReferenceToClassFunctionsRuleConfig,
noReferenceToModuleFunctionsRuleConfig,
noGetterContainsMoreThanReturnStatementRuleConfig,
noUnsupportedMemberVariableInMemberExpressionRuleConfig,
];
Loading
Loading