#2013

xlsx: external_links_at_risk misses worksheet-scoped names and aliases to external references

OpenCloneOfAlexopened 18d ago

Summary

external_links_at_risk detects a direct workbook-level defined name referring to an external workbook, but misses an equivalent worksheet-scoped name and a workbook-level alias to that name. These false negatives let missing external caches pass the pre-recalculation safeguard.

Tested at dbd4588f9e1033efb41dad4bef2f7947c8993d44 with Python 3.12.14 and openpyxl 3.1.5.

Reproduction

Run from the repository root. Fixtures contain real external-link parts and relationships; the formulas have no cached values.

import sys, tempfile
from pathlib import Path
from openpyxl import Workbook
from openpyxl.workbook.defined_name import DefinedName
from openpyxl.workbook.external_link.external import ExternalLink, ExternalBook, ExternalSheetNames
from openpyxl.packaging.relationship import Relationship
sys.path.insert(0, str(Path.cwd() / "skills/xlsx/scripts"))
from recalc import external_links_at_risk

with tempfile.TemporaryDirectory() as tmp:
    for variant in ("direct", "alias", "local", "ordinary"):
        wb = Workbook()
        ws = wb.active
        ws.title = "Main"
        ext = ExternalLink(externalBook=ExternalBook(
            sheetNames=ExternalSheetNames(sheetName=["Sheet1"]), id="rId1"))
        ext.file_link = Relationship(
            Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/externalLinkPath",
            Target="file:///audit-missing-source.xlsx", TargetMode="External", Id="rId1")
        wb._external_links.append(ext)
        if variant == "ordinary":
            ws["A1"] = "=1+1"
        elif variant == "local":
            ws.defined_names.add(DefinedName("ExternalAmount",
                attr_text="'[1]Sheet1'!$A$1", localSheetId=0))
            ws["A1"] = "=ExternalAmount"
        else:
            wb.defined_names.add(DefinedName("ExternalAmount",
                attr_text="'[1]Sheet1'!$A$1"))
            if variant == "alias":
                wb.defined_names.add(DefinedName("AliasAmount", attr_text="ExternalAmount"))
                ws["A1"] = "=AliasAmount"
            else:
                ws["A1"] = "=ExternalAmount"
        path = Path(tmp) / (variant + ".xlsx")
        wb.save(path)
        print(variant, external_links_at_risk(path))

Actual result

direct ['Main!A1']
alias []
local []
ordinary []

All four fixtures round-trip with one external link in openpyxl. Direct, alias and local A1 caches are None.

Expected result

Both alias and local should flag Main!A1, as direct does. The ordinary formula should remain unflagged.

Cause and suggested correction

recalc.py iterates workbook-level defined_names and checks each name's value for a direct external reference. It neither includes worksheet-local names nor follows name-to-name dependencies.

Resolve names with worksheet scope and shadowing, follow aliases with cycle detection, and compare names case-insensitively. Add direct/local/alias/ordinary regression fixtures.

Impact and limits

The demonstrated issue is a false negative in the safeguard used before in-place LibreOffice recalculation when force=False. It can allow a workbook with unresolved external dependencies to reach conversion. LibreOffice was not run in this reproduction, so actual cache loss or workbook damage is not claimed as observed.

Related #1464/#1517 concern case-insensitive matching of global names. This reproduction uses matching case throughout, and the inspected #1517 patch does not address scope or aliases. Targeted public issue/PR review found no matching report for these cases.

Activity

0

No activity yet.